import java.util.*;

class Node {
    public int val;
    public Node left;
    public Node right;
    public Node next;

    public Node() {
    }

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val, Node _left, Node _right, Node _next) {
        val = _val;
        left = _left;
        right = _right;
        next = _next;
    }
}

class SelectedTopInterviewSolution {

    // 1.两数之和
    // 给定一个整数数组 nums 和一个整数目标值 target，请你在该数组中找出 和为目标值 target  的那 两个 整数，并返回它们的数组下标。
    // 你可以假设每种输入只会对应一个答案。但是，数组中同一个元素在答案里不能重复出现。
    // 你可以按任意顺序返回答案。
    // 提示：
    // 2 <= nums.length <= 104
    // -109 <= nums[i] <= 109
    // -109 <= target <= 109
    // 只会存在一个有效答案
    // 你可以想出一个时间复杂度小于 O(n2) 的算法吗？

    // 方法一：暴力枚举-时间复杂度：O(n^2)，空间复杂度：O(1)
    public int[] twoSum(int[] nums, int target) {
        int n = nums.length;
        for (int i = 0; i < n; ++i)
            for (int j = i + 1; j < n; ++j)
                if (nums[i] + nums[j] == target)
                    return new int[] { i, j };

        return new int[0];
    }

    // 方法二：哈希表-时空复杂度：O(n)（空间换取时间）
    public int[] twoSum2(int[] nums, int target) {

        Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
        // 建表与查表同时进行，因为答案唯一a + b = target，先到a或b都行
        for (int i = 0; i < nums.length; ++i) {
            if (hashtable.containsKey(target - nums[i]))
                return new int[] { hashtable.get(target - nums[i]), i };

            hashtable.put(nums[i], i);
        }
        return new int[0];
    }

    // 2.两数相加
    // 给你两个 非空 的链表，表示两个非负的整数。
    // 它们每位数字都是按照 逆序 的方式存储的，并且每个节点只能存储 一位 数字。
    // 请你将两个数相加，并以相同形式返回一个表示和的链表。
    // 你可以假设除了数字 0 之外，这两个数都不会以 0 开头。
    // 提示：
    // 每个链表中的节点数在范围 [1, 100] 内
    // 0 <= Node.val <= 9
    // 题目数据保证列表表示的数字不含前导零

    // 方法一：（自己写的）迭代（遍历链表）-时间复杂度：O(n)，空间复杂度：O(1)
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode dummyHead = new ListNode(-1);
        ListNode curr = dummyHead;
        int adder = 0;
        while (l1 != null || l2 != null || adder != 0) {
            int l1Val = 0;
            if (l1 != null) {
                l1Val = l1.val;
                l1 = l1.next;
            }
            int l2Val = 0;
            if (l2 != null) {
                l2Val = l2.val;
                l2 = l2.next;
            }
            int sum = l1Val + l2Val + adder;
            adder = sum / 10;
            int val = sum % 10;
            curr.next = new ListNode(val);
            curr = curr.next;
        }
        return dummyHead.next;
    }

    // 3.无重复字符的最长子串
    // 给定一个字符串 s ，请你找出其中不含有重复字符的 最长子串 的长度。
    // 提示：
    // 0 <= s.length <= 5 * 104
    // s 由英文字母、数字、符号和空格组成

    // 方法一：滑动窗口-时间复杂度：O(N)，空间复杂度：O(∣Σ∣)，∣Σ∣ 表示字符集的大小。
    // 优化：使用HashMap一并存储所在索引，左指针直接跳到索引处
    public int lengthOfLongestSubstring(String s) {
        HashMap<Character, Integer> map = new HashMap<>();
        int max = 0, start = 0;
        for (int end = 0; end < s.length(); end++) {
            char ch = s.charAt(end);
            if (map.containsKey(ch))
                start = Math.max(map.get(ch) + 1, start);// 精髓所在，存在于map，但是不在[left, right]范围内，直接更新map就好

            max = Math.max(max, end - start + 1);
            map.put(ch, end);
        }
        return max;
    }

    // 4.寻找两个正序数组的中位数
    // 给定两个大小分别为 m 和 n 的正序（从小到大）数组 nums1 和 nums2。
    // 请你找出并返回这两个正序数组的 中位数 。
    // 算法的时间复杂度应该为 O(log (m+n)) 。
    // 提示：
    // nums1.length == m
    // nums2.length == n
    // 0 <= m <= 1000
    // 0 <= n <= 1000
    // 1 <= m + n <= 2000
    // -106 <= nums1[i], nums2[i] <= 106
    // 如果对时间复杂度的要求有 log，通常都需要用到二分查找

    // 方法一：二分查找-时间复杂度：O(log(m+n))，空间复杂度：O(1)
    // 这道题可以转化成寻找两个有序数组中的第 k 小的数，其中 k 为 (m+n)/2 或 (m+n)/2+1。
    // 如果 A[k/2−1]<B[k/2−1]，则比A[k/2−1] 小的数最多只有 A 的前 k/2−1 个数和 B 的前 k/2−1 个数，
    // 即比 A[k/2−1] 小的数最多只有 k-2 个，因此 A[k/2−1] 不可能是第 k 个数，
    // A[0] 到 A[k/2−1] 也都不可能是第 k 个数，可以全部排除。
    // 如果 A[k/2−1]>B[k/2−1]，则可以排除 B[0] 到 B[k/2−1]。
    // 如果 A[k/2−1]=B[k/2−1]，则可以归入第一种情况处理。
    // 可以看到，比较 A[k/2−1] 和 B[k/2−1] 之后，可以排除 k/2 个不可能是第 k 小的数，查找范围缩小了一半。
    public double findMedianSortedArrays(int[] nums1, int[] nums2) {
        int length1 = nums1.length, length2 = nums2.length;
        int totalLength = length1 + length2;
        if (totalLength % 2 == 1) {// 总长度为奇数，中位数索引为midIndex
            int midIndex = totalLength / 2;
            double median = getKthElement(nums1, nums2, midIndex + 1);
            return median;
        } else {// 总长度为偶数，中位数为索引midIndex1 + midIndex2的平均值
            int midIndex1 = totalLength / 2 - 1, midIndex2 = totalLength / 2;
            double median = (getKthElement(nums1, nums2, midIndex1 + 1) +
                    getKthElement(nums1, nums2, midIndex2 + 1)) / 2.0;
            return median;
        }
    }

    public int getKthElement(int[] nums1, int[] nums2, int k) {
        // 主要思路：要找到第 k (k>1) 小的元素，
        // 那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
        // 这里的 "/" 表示整除
        // nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
        // nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
        // 取 pivot = min(pivot1, pivot2)，
        // 两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
        // 这样 pivot 本身最大也只能是第 k-1 小的元素
        // 如果 pivot = pivot1，那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。
        // 把这些元素全部 "删除"，剩下的作为新的 nums1 数组
        // 如果 pivot = pivot2，那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。
        // 把这些元素全部 "删除"，剩下的作为新的 nums2 数组
        // 由于我们 "删除" 了一些元素（这些元素都比第 k 小的元素要小），因此需要修改 k 的值，减去删除的数的个数
        int length1 = nums1.length, length2 = nums2.length;
        int index1 = 0, index2 = 0;

        while (true) {
            // 边界情况
            if (index1 == length1)
                return nums2[index2 + k - 1];
            if (index2 == length2)
                return nums1[index1 + k - 1];
            if (k == 1)
                return Math.min(nums1[index1], nums2[index2]);

            // 正常情况
            int half = k / 2;
            int newIndex1 = Math.min(index1 + half, length1) - 1;
            int newIndex2 = Math.min(index2 + half, length2) - 1;
            int pivot1 = nums1[newIndex1], pivot2 = nums2[newIndex2];
            if (pivot1 <= pivot2) {// 排除 A[index1] 到 A[k/2−1]
                k -= (newIndex1 - index1 + 1);
                index1 = newIndex1 + 1;// A数组下标移动
            } else {// 排除 B[index2] 到 B[k/2−1]
                k -= (newIndex2 - index2 + 1);
                index2 = newIndex2 + 1;// B数组下标移动
            }
        }
    }

    // 方法一：（自己写的）二分查找-时间复杂度：O(log(m+n))，空间复杂度：O(1)
    public double findMedianSortedArrays11(int[] nums1, int[] nums2) {
        int m = nums1.length, n = nums2.length;
        int nums1Left = 0, nums2Left = 0;
        int needRemoved = (m + n - 1) / 2;
        while (needRemoved > 0) {
            int removed = Math.max(needRemoved / 2, 1);
            int nums1Mid = nums1Left + removed - 1;
            int nums2Mid = nums2Left + removed - 1;
            int num1 = nums1Mid < m ? nums1[nums1Mid] : Integer.MAX_VALUE;
            int num2 = nums2Mid < n ? nums2[nums2Mid] : Integer.MAX_VALUE;
            if (num1 <= num2)
                nums1Left = nums1Mid + 1;
            else
                nums2Left = nums2Mid + 1;
            needRemoved -= removed;
        }

        PriorityQueue<Integer> pq = new PriorityQueue<>();
        if (nums1Left < m)
            pq.offer(nums1[nums1Left]);
        if (nums1Left + 1 < m)
            pq.offer(nums1[nums1Left + 1]);
        if (nums2Left < n)
            pq.offer(nums2[nums2Left]);
        if (nums2Left + 1 < n)
            pq.offer(nums2[nums2Left + 1]);

        if (((m + n) & 1) == 1)
            return pq.poll();
        else
            return (pq.poll() + pq.poll()) / 2.0;
    }

    // 方法二：划分数组（其实也是二分）-时间复杂度：O(logmin(m,n))，空间复杂度：O(1)
    // 划分思想：中位数，即是将一个集合划分为两个长度相等的子集，其中一个子集中的元素总是大于另一个子集中的元素。
    // nums1 num2分别拆为左右两部分，左右两部分分别合并，得到A, B
    // 当 A 和 B 的总长度是偶数时：
    // 保证A B长度相等，median = (A最大 + B最小) / 2
    // 当 A 和 B 的总长度是奇数时：
    // 保证 len(A) = len(B) + 1，median = A最大
    // i, j 是nums1 num2的拆分点
    // 则：
    // i+j=m−i+n−j（当 m+n 为偶数）或 i+j+1=m−i+n−j（当 m+n 为奇数）二分搜索i值
    // 还要保证：前一部分的最大值小于等于后一部分的最小值
    public double findMedianSortedArrays2(int[] nums1, int[] nums2) {
        // 保证第一个数组长度比第二个数组短(或者相等)，因为二分查找是以nums1的边界作为参考，nums2根据恒等式得到j的值
        // 如果nums1长度大于nums2，那么j可能会计算出为负
        if (nums1.length > nums2.length)
            return findMedianSortedArrays2(nums2, nums1);

        int m = nums1.length;
        int n = nums2.length;
        int left = 0, right = m;
        // median1：前一部分的最大值
        // median2：后一部分的最小值
        int firstHalfMax = 0, secondHalfMin = 0;

        while (left <= right) {
            // 前一部分包含 nums1[0 .. i-1] 和 nums2[0 .. j-1]
            // 后一部分包含 nums1[i .. m-1] 和 nums2[j .. n-1]
            int i = (left + right) / 2;// 二分搜索i值
            int j = (m + n) / 2 - i;

            // nums_im1, nums_i, nums_jm1, nums_j
            // 分别表示 nums1[i-1], nums1[i], nums2[j-1], nums2[j]
            // 用最小最大值充当边界nums[-1] num[m or n]
            int nums_im1 = (i == 0 ? Integer.MIN_VALUE : nums1[i - 1]);
            int nums_i = (i == m ? Integer.MAX_VALUE : nums1[i]);
            int nums_jm1 = (j == 0 ? Integer.MIN_VALUE : nums2[j - 1]);
            int nums_j = (j == n ? Integer.MAX_VALUE : nums2[j]);

            if (nums_im1 <= nums_j) {// nums1[i-1]<= nums2[j] 这是满足数组划分的一种情况，记录此时的中位数
                firstHalfMax = Math.max(nums_im1, nums_jm1);
                secondHalfMin = Math.min(nums_i, nums_j);
                left = i + 1;
            } else
                right = i - 1;
        }

        return (m + n) % 2 == 0 ? (firstHalfMax + secondHalfMin) / 2.0 : secondHalfMin;
    }

    // 5.最长回文子串
    // 方法一：动态规划（有点强行了）-时间复杂度：O(n^2)，空间复杂度：O(n^2)

    // 方法二：（自己写的）中心拓展-时间复杂度：O(n^2)，空间复杂度：O(1)
    public String longestPalindrome(String s) {
        if (s.length() == 1)
            return s;
        String res = "";
        for (int i = 0; i < s.length() - 1; i++) {
            String ans1 = getPalindrome(i, i, s);
            String ans2 = getPalindrome(i, i + 1, s);
            if (ans1.length() > res.length())
                res = ans1;
            if (ans2.length() > res.length())
                res = ans2;
        }
        return res;
    }

    private String getPalindrome(int left, int right, String s) {
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            left--;
            right++;
        }
        return s.substring(left + 1, right);
    }

    // 方法三：Manacher 算法-时间复杂度：O(n)，空间复杂度：O(n)
    // 定义一个新概念臂长，表示中心扩展算法向外扩展的长度。如果一个位置的最大回文字符串长度为 2 * length + 1 ，其臂长为 length。
    // 在中心扩展法的过程中记录右臂在最右边的回文字符串，将其中心作为 j，在计算过程中就能最大限度地避免重复计算。
    public String longestPalindrome3(String s) {
        int n = s.length();
        StringBuffer t = new StringBuffer("$#");
        // 解决回文串奇数长度和偶数长度的问题，处理方式是在所有的相邻字符中间插入 # ，这样可以保证所有找到的回文串都是奇数长度的
        for (int i = 0; i < n; ++i)
            t.append(s.charAt(i)).append('#');

        t.append('!');// 字符串边界外，再随便放两个不一样的字符，防越界。e.g. aba → $#a#b#a#!
        n = t.length();

        int[] armLen = new int[n];
        int j = 0, right = 0;// 维护「当前最大的回文的右端点right」以及这个回文右端点对应的回文中心
        int iMax = 0, maxArmLen = 0;// 最长回文串的回文中心和臂长

        for (int i = 2; i < n - 2; ++i) {
            // i 被包含在当前最大回文子串内(right与当前点的距离, i关于j对称的点的armLen值)，不被包含(0)
            // 这里将 right−i 和 armLen[对称点] 取小，是先要保证这个回文串在当前最大回文串内。
            armLen[i] = i <= right ? Math.min(right - i, armLen[j * 2 - i]) : 0;// 初始化（马拉车算法的精华所在）
            while (t.charAt(i + armLen[i] + 1) == t.charAt(i - armLen[i] - 1))// 中心拓展
                ++armLen[i];
            if (i + armLen[i] > right) {// 动态维护 iMax 和 rMax
                j = i;
                right = i + armLen[i];
            }
            if (armLen[i] > maxArmLen) {// 记录当前最长回文串
                iMax = i;
                maxArmLen = armLen[i];
            }
        }

        // 去掉# 返回答案
        StringBuffer ans = new StringBuffer();
        for (int i = iMax - maxArmLen; i < iMax + maxArmLen; ++i)
            if (t.charAt(i) != '#')
                ans.append(t.charAt(i));

        return ans.toString();
    }

    // 7.整数反转
    // 给你一个 32 位的有符号整数 x ，返回将 x 中的数字部分反转后的结果。
    // 如果反转后整数超过 32 位的有符号整数的范围 [−231,  231 − 1] ，就返回 0。
    // 假设环境不允许存储 64 位整数（有符号或无符号）。
    // 提示：
    // -231 <= x <= 231 - 1

    // 方法一：数学-时间复杂度：O(1)，空间复杂度：O(1)
    public int reverse(int x) {
        int rev = 0;
        while (x != 0) {
            // 不用判断 rev == Integer.MIN_VALUE / 10 || rev == Integer.MIN_VALUE / 10 的情况
            // Integer.MAX_VALUE 2^31-1 2147483647
            // Integer.MIN_VALUE -2^31 -2147483648
            if (rev < Integer.MIN_VALUE / 10 || rev > Integer.MAX_VALUE / 10)
                return 0;
            int digit = x % 10;
            x /= 10;
            rev = rev * 10 + digit;
        }
        return rev;
    }

    // 方法一：（自己写的）数学-时间复杂度：O(1)，空间复杂度：O(1)
    public int reverse11(int x) {
        if (x == Integer.MIN_VALUE)
            return 0;

        int sign = 1;
        if (x < 0) {
            x = -x;
            sign = -1;
        }

        int res = 0;
        while (x != 0) {
            if (res > Integer.MAX_VALUE / 10)
                return 0;

            int cur = x % 10;
            x = x / 10;
            res = res * 10 + cur;
        }
        return sign * res;
    }

    // 8.字符串转换整数(atoi)
    // 请你来实现一个 myAtoi(string s) 函数，使其能将字符串转换成一个 32 位有符号整数（类似 C/C++ 中的 atoi 函数）。
    // 函数 myAtoi(string s) 的算法如下：
    // 读入字符串并丢弃无用的前导空格
    // 检查下一个字符（假设还未到字符末尾）为正还是负号，读取该字符（如果有）。
    // 确定最终结果是负数还是正数。 如果两者都不存在，则假定结果为正。
    // 读入下一个字符，直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
    // 将前面步骤读入的这些数字转换为整数（即，"123" -> 123， "0032" -> 32）。如果没有读入数字，则整数为 0 。
    // 必要时更改符号（从步骤 2 开始）。
    // 如果整数数超过 32 位有符号整数范围 [−231,  231 − 1] ，需要截断这个整数，使其保持在这个范围内。
    // 具体来说，小于 −231 的整数应该被固定为 −231 ，大于 231 − 1 的整数应该被固定为 231 − 1 。
    // 返回整数作为最终结果。
    // 注意：
    // 本题中的空白字符只包括空格字符 ' ' 。
    // 除前导空格或数字后的其余字符串外，请勿忽略 任何其他字符。
    // 提示：
    // 0 <= s.length <= 200
    // s 由英文字母（大写和小写）、数字（0-9）、' '、'+'、'-' 和 '.' 组成

    // 方法一：（自己写的）模拟 if else-时间复杂度：O(n)，空间复杂度：O(1)
    public int myAtoi(String s) {
        s = s.trim();
        int n = s.length();
        boolean hasSign = false, isNegtive = false;
        int res = 0;
        for (int i = 0; i < n; i++) {
            char c = s.charAt(i);
            if (c == '-') {
                if (hasSign)
                    break;
                hasSign = true;
                isNegtive = true;
            } else if (c == '+') {
                if (hasSign)
                    break;
                hasSign = true;
            } else if (Character.isDigit(c)) {
                hasSign = true;
                int cur = c - '0';
                if (res > Integer.MAX_VALUE / 10)
                    return isNegtive ? Integer.MIN_VALUE : Integer.MAX_VALUE;
                else if (res == Integer.MAX_VALUE / 10) {
                    if (isNegtive && cur >= -(Integer.MIN_VALUE % 10))
                        return Integer.MIN_VALUE;
                    if (!isNegtive && cur >= Integer.MAX_VALUE % 10)
                        return Integer.MAX_VALUE;
                }
                res = res * 10 + cur;
            } else
                break;
        }
        if (isNegtive)
            res = -res;
        return res;
    }

    // 方法二：有限状态机-时间复杂度：O(n)，空间复杂度：O(1)
    public int strToInt2(String str) {
        Automaton automaton = new Automaton();
        int length = str.length();
        for (int i = 0; i < length; ++i)
            automaton.get(str.charAt(i));

        return (int) (automaton.sign * automaton.ans);
    }

    class Automaton {
        public int sign = 1;// 符号标记
        public long ans = 0;
        private String state = "start";// 起始状态
        private Map<String, String[]> table = new HashMap<String, String[]>() {
            {
                // 状态转移方式 ' ' +/- number other
                // 起始状态
                put("start", new String[] { "start", "signed", "in_number", "end" });
                // 符号
                put("signed", new String[] { "end", "end", "in_number", "end" });
                // 数字
                put("in_number", new String[] { "end", "end", "in_number", "end" });
                // 终止状态
                put("end", new String[] { "end", "end", "end", "end" });
            }
        };

        // 状态转移
        public void get(char c) {
            state = table.get(state)[get_col(c)];
            if ("in_number".equals(state)) {// 数字状态，计算答案
                ans = ans * 10 + c - '0';
                ans = sign == 1 ? Math.min(ans, (long) Integer.MAX_VALUE) : Math.min(ans, -(long) Integer.MIN_VALUE);
            } else if ("signed".equals(state))// 符号状态，记录正负
                sign = c == '+' ? 1 : -1;

        }

        // 状态转移方式 ' ' +/- number other
        private int get_col(char c) {
            if (c == ' ')
                return 0;
            if (c == '+' || c == '-')
                return 1;
            if (Character.isDigit(c))
                return 2;
            return 3;
        }
    }

    // 10.正则表达式匹配
    // 给你一个字符串 s 和一个字符规律 p，请你来实现一个支持 '.' 和 '*' 的正则表达式匹配。
    // '.' 匹配任意单个字符
    // '*' 匹配零个或多个前面的那一个元素
    // 所谓匹配，是要涵盖 整个 字符串 s的，而不是部分字符串。
    // 提示：
    // 1 <= s.length <= 20
    // 1 <= p.length <= 30
    // s 可能为空，且只包含从 a-z 的小写字母。
    // p 可能为空，且只包含从 a-z 的小写字母，以及字符 . 和 *。
    // 保证每次出现字符 * 时，前面都匹配到有效的字符
    // 理解：ab .* true

    // 方法一：动态规划-时间复杂度、空间复杂度：O(mn)
    // 对匹配的方案进行枚举：
    // f[i][j] 表示 s 的前 i 个字符与 p 中的前 j 个字符是否能够匹配。
    public boolean isMatch(String s, String p) {
        int m = s.length();
        int n = p.length();

        boolean[][] f = new boolean[m + 1][n + 1];
        f[0][0] = true;// 初始化，方便后面的状态转移
        for (int i = 0; i <= m; ++i)// 前 i 个字符
            for (int j = 1; j <= n; ++j) {// 前 j 个字符
                if (p.charAt(j - 1) == '*') {// 第j个字符为*（对应下标j-1）
                    // *前字符匹配 s 末尾的一个字符，将该字符扔掉，而该组合还可以继续进行匹配，
                    // 继承上一状态（看做出现次数+1，或者成功了也不匹配）
                    if (matches(s, p, i, j - 1))
                        f[i][j] = f[i - 1][j] || f[i][j - 2];

                    // *前字符不匹配字符，将该组合扔掉，不再进行匹配，继承上一状态（看做是出现次数为0）
                    else
                        f[i][j] = f[i][j - 2];

                } else {// 第j个字符为字母或.
                    if (matches(s, p, i, j))
                        f[i][j] = f[i - 1][j - 1];// 匹配则继承上一状态（单字符匹配）
                    // 不匹配则为默认值fasle
                }
            }

        return f[m][n];
    }

    // 字符的比较（字符串i长度为0，一定不匹配）
    public boolean matches(String s, String p, int i, int j) {
        if (i == 0)// 字符串长度为0，一定不匹配
            return false;

        if (p.charAt(j - 1) == '.')// 一定匹配
            return true;

        return s.charAt(i - 1) == p.charAt(j - 1);
    }

    // 方法一：（自己写的）动态规划-时间复杂度、空间复杂度：O(mn)
    public boolean isMatch11(String s, String p) {
        int m = s.length(), n = p.length();
        boolean[][] dp = new boolean[m + 1][n + 1];
        dp[0][0] = true;
        for (int j = 1; j <= n; j++)
            if (p.charAt(j - 1) == '*')
                dp[0][j] = dp[0][j - 2];

        for (int i = 1; i <= m; i++) {
            char c = s.charAt(i - 1);
            for (int j = 1; j <= n; j++) {
                char sign = p.charAt(j - 1);
                if (sign == '*') {
                    char prevChar = p.charAt(j - 2);
                    dp[i][j] = dp[i][j - 2];
                    if (c == prevChar || prevChar == '.')
                        dp[i][j] = dp[i][j] || dp[i - 1][j];
                } else if (sign == '.') {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    if (c == sign)
                        dp[i][j] = dp[i - 1][j - 1];
                }
            }
        }
        return dp[m][n];
    }

    // 11.盛最多水的容器
    // 13.罗马数字转整数
    // 罗马数字包含以下七种字符: I， V， X， L，C，D 和 M。
    // 字符 数值
    // I 1
    // V 5
    // X 10
    // L 50
    // C 100
    // D 500
    // M 1000
    // 例如， 罗马数字 2 写做 II ，即为两个并列的 1 。12 写做 XII ，即为 X + II 。 27 写做  XXVII,
    // 即为 XX + V + II 。
    // 通常情况下，罗马数字中小的数字在大的数字的右边。
    // 但也存在特例，例如 4 不写做 IIII，而是 IV。数字 1 在数字 5 的左边，所表示的数等于大数 5 减小数 1 得到的数值 4 。
    // 同样地，数字 9 表示为 IX。这个特殊的规则只适用于以下六种情况：
    // I 可以放在 V (5) 和 X (10) 的左边，来表示 4 和 9。
    // X 可以放在 L (50) 和 C (100) 的左边，来表示 40 和 90。 
    // C 可以放在 D (500) 和 M (1000) 的左边，来表示 400 和 900。
    // 给定一个罗马数字，将其转换成整数。
    // 提示：
    // 1 <= s.length <= 15
    // s 仅含字符 ('I', 'V', 'X', 'L', 'C', 'D', 'M')
    // 题目数据保证 s 是一个有效的罗马数字，且表示整数在范围 [1, 3999] 内
    // 题目所给测试用例皆符合罗马数字书写规则，不会出现跨位等情况。
    // IL 和 IM 这样的例子并不符合题目要求，49 应该写作 XLIX，999 应该写作 CMXCIX 。
    // 关于罗马数字的详尽书写规则，可以参考 罗马数字 - Mathematics 。

    // 方法一：模拟-时间复杂度：O(n)，空间复杂度：O(1)
    Map<Character, Integer> symbolValues = new HashMap<Character, Integer>() {
        {
            put('I', 1);
            put('V', 5);
            put('X', 10);
            put('L', 50);
            put('C', 100);
            put('D', 500);
            put('M', 1000);
        }
    };

    public int romanToInt(String s) {
        int ans = 0;
        int n = s.length();
        for (int i = 0; i < n; ++i) {
            int value = symbolValues.get(s.charAt(i));
            // 当前位字母小于下一位字母，则一定是形如IV IX的组合，先减掉当前值
            if (i < n - 1 && value < symbolValues.get(s.charAt(i + 1)))
                ans -= value;
            else
                ans += value;
        }
        return ans;
    }

    // 方法一：（自己写的dfs）模拟-时间复杂度：O(n)，空间复杂度：O(1)
    class ssoltion_13 {
        boolean find = false;
        int res = 0;
        Map<String, int[]> map = new HashMap<>() {
            {
                put("I", new int[] { 1, 1 });
                put("IV", new int[] { 4, 5 });
                put("V", new int[] { 5, 5 });
                put("IX", new int[] { 9, 10 });
                put("X", new int[] { 10, 10 });
                put("XL", new int[] { 40, 50 });
                put("L", new int[] { 50, 50 });
                put("XC", new int[] { 90, 100 });
                put("C", new int[] { 100, 100 });
                put("CD", new int[] { 400, 500 });
                put("D", new int[] { 500, 500 });
                put("CM", new int[] { 900, 1000 });
                put("M", new int[] { 1000, 1000 });
            }
        };

        public int romanToInt(String s) {
            dfs(0, 5000, 0, s);
            return res;
        }

        private void dfs(int index, int prevRange, int ans, String s) {
            if (find)// 已找到答案，提前返回
                return;

            int n = s.length();
            if (index == n) {// 记录最终答案，标记已找到答案
                find = true;
                res = ans;
                return;
            }

            String curr = "";// 记录当前层的罗马符号组合
            for (int i = index; i < n; i++) {
                curr += s.charAt(i);
                if (!map.containsKey(curr))// 没有对应罗马符号组合，提前退出
                    break;

                int[] romanNumber = map.get(curr);
                int currRange = romanNumber[1];
                if (currRange > prevRange)// 当前层罗马数字大小，大于上一层罗马数字大小，提前退出
                    break;

                dfs(i + 1, currRange, ans + romanNumber[0], s);
            }
        }
    }

    // 14.最长公共前缀
    // 编写一个函数来查找字符串数组中的最长公共前缀。
    // 如果不存在公共前缀，返回空字符串 ""。
    // 提示：
    // 1 <= strs.length <= 200
    // 0 <= strs[i].length <= 200
    // strs[i] 仅由小写英文字母组成

    // 方法一：（自己写的）横向扫描-时间复杂度：O(mn)，空间复杂度：O(1)，其中 m 是字符串数组中的字符串的平均长度，n 是字符串的数量。
    // 依次遍历字符串数组中的每个字符串，对于每个遍历到的字符串，更新最长公共前缀，
    // 当遍历完所有的字符串以后，即可得到字符串数组中的最长公共前缀。
    // 官方写的复杂了...
    public String longestCommonPrefix(String[] strs) {
        String ans = strs[0];
        int end = ans.length();
        for (int i = 1; i < strs.length; i++) {
            String str = strs[i];
            for (int j = 0; j < Math.min(end, str.length()); j++) {
                if (str.charAt(j) != ans.charAt(j)) {
                    end = j;
                    break;
                }
            }
            end = Math.min(end, str.length());
        }

        return ans.substring(0, end);
    }

    // 方法二：纵向扫描-时间复杂度：O(mn)，空间复杂度：O(1)，其中 m 是字符串数组中的字符串的平均长度，n 是字符串的数量。
    // 从前往后遍历所有字符串的每一列，比较相同列上的字符是否相同，
    // 如果相同则继续对下一列进行比较，如果不相同则当前列不再属于公共前缀，当前列之前的部分为最长公共前缀。
    public String longestCommonPrefix2(String[] strs) {
        if (strs == null || strs.length == 0)
            return "";

        int length = strs[0].length();
        int count = strs.length;
        for (int i = 0; i < length; i++) {
            char c = strs[0].charAt(i);
            for (int j = 1; j < count; j++)
                if (i == strs[j].length() || strs[j].charAt(i) != c)
                    return strs[0].substring(0, i);
        }
        return strs[0];
    }

    // 方法三：dfs 分治-时间复杂度：O(mn)，空间复杂度：O(mlogn)，其中 m 是字符串数组中的字符串的平均长度，n 是字符串的数量。
    public String longestCommonPrefix3(String[] strs) {
        if (strs == null || strs.length == 0)
            return "";
        else
            return longestCommonPrefix(strs, 0, strs.length - 1);

    }

    public String longestCommonPrefix(String[] strs, int start, int end) {
        if (start == end)
            return strs[start];
        else {
            int mid = (end - start) / 2 + start;
            String lcpLeft = longestCommonPrefix(strs, start, mid);
            String lcpRight = longestCommonPrefix(strs, mid + 1, end);
            return commonPrefix(lcpLeft, lcpRight);
        }
    }

    public String commonPrefix(String lcpLeft, String lcpRight) {
        int minLength = Math.min(lcpLeft.length(), lcpRight.length());
        for (int i = 0; i < minLength; i++)
            if (lcpLeft.charAt(i) != lcpRight.charAt(i))
                return lcpLeft.substring(0, i);

        return lcpLeft.substring(0, minLength);
    }

    // 方法四：二分查找-时间复杂度：O(mnlogm)，空间复杂度：O(mlogn)，其中 m 是字符串数组中的字符串的平均长度，n 是字符串的数量。
    // 最长公共前缀的长度不会超过字符串数组中的最短字符串的长度。
    // 用 minLength 表示字符串数组中的最短字符串的长度，则可以在 [0,minLength] 的范围内通过二分查找得到最长公共前缀的长度。
    public String longestCommonPrefix4(String[] strs) {
        if (strs == null || strs.length == 0) {
            return "";
        }
        int minLength = Integer.MAX_VALUE;
        for (String str : strs) {
            minLength = Math.min(minLength, str.length());
        }
        int low = 0, high = minLength;
        while (low < high) {
            int mid = (high - low + 1) / 2 + low;
            if (isCommonPrefix(strs, mid)) {
                low = mid;
            } else {
                high = mid - 1;
            }
        }
        return strs[0].substring(0, low);
    }

    public boolean isCommonPrefix(String[] strs, int length) {
        String str0 = strs[0].substring(0, length);
        int count = strs.length;
        for (int i = 1; i < count; i++) {
            String str = strs[i];
            for (int j = 0; j < length; j++) {
                if (str0.charAt(j) != str.charAt(j)) {
                    return false;
                }
            }
        }
        return true;
    }

    // 方法五：（自己写的）排序 + 比较最短最长-时间复杂度：O(mnlogn)，空间复杂度：O(logn)，其中 m 是字符串数组中的字符串的平均长度，n
    // 是字符串的数量。
    public String longestCommonPrefix5(String[] strs) {
        Arrays.sort(strs);
        String str1 = strs[0];
        String str2 = strs[strs.length - 1];
        for (int i = 0; i < str1.length(); i++)
            if (str1.charAt(i) != str2.charAt(i))
                return str1.substring(0, i);

        return str1;
    }

    // 15.三数之和
    // 给你一个整数数组 nums ，判断是否存在三元组 [nums[i], nums[j], nums[k]]
    // 满足 i != j、i != k 且 j != k ，同时还满足 nums[i] + nums[j] + nums[k] == 0 。
    // 请你返回所有和为 0 且不重复的三元组。
    // 注意：答案中不可以包含重复的三元组。
    // 提示：
    // 3 <= nums.length <= 3000
    // -105 <= nums[i] <= 105

    // 方法一：排序 + 双指针（有点牵强）-时间复杂度：O(n^2)，空间复杂度：O(logn)
    // 不包含重复的三元组的关键：需要和上一次枚举的数不相同
    public List<List<Integer>> threeSum(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        // a + b + c = 0
        // 枚举 a
        for (int first = 0; first < n; ++first) {
            // 需要和上一次枚举的数不相同
            if (first > 0 && nums[first] == nums[first - 1])
                continue;

            // c 对应的指针初始指向数组的最右端
            int third = n - 1;
            int target = -nums[first]; // b + c = -a = target
            // 枚举 b
            for (int second = first + 1; second < n; ++second) {
                // 需要和上一次枚举的数不相同
                if (second > first + 1 && nums[second] == nums[second - 1])
                    continue;

                // 需要保证 b 的指针在 c 的指针的左侧
                while (second < third && nums[second] + nums[third] > target)
                    --third;

                // 如果指针重合，随着 b 后续的增加
                // 就不会有满足 a+b+c=0 并且 b<c 的 c 了，可以退出循环
                if (second == third)
                    break;

                // 所得即答案
                if (nums[second] + nums[third] == target) {
                    List<Integer> list = new ArrayList<Integer>();
                    list.add(nums[first]);
                    list.add(nums[second]);
                    list.add(nums[third]);
                    ans.add(list);
                }
            }
        }
        return ans;
    }

    // 方法一：自己写的（真正的双指针）-时间复杂度：O(n^2)，空间复杂度：O(logn)
    public List<List<Integer>> threeSum11(int[] nums) {
        Arrays.sort(nums);
        int n = nums.length;
        List<List<Integer>> res = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (i > 0 && nums[i] == nums[i - 1])
                continue;
            int a = nums[i];
            int leftIndex = i + 1;
            int rightIndex = n - 1;
            while (leftIndex < rightIndex) {
                int left = nums[leftIndex];
                int right = nums[rightIndex];
                int sum = a + left + right;
                if (sum == 0) {
                    res.add(new ArrayList<>(Arrays.asList(a, left, right)));
                    // 避免出现重复答案
                    while (leftIndex < n && nums[leftIndex] == left)
                        leftIndex++;
                } else if (sum > 0)
                    rightIndex--;
                else
                    leftIndex++;
            }
        }
        return res;
    }

    // 方法二：排序 + 二分查找-时间复杂度：O(n^2logn)，空间复杂度：O(logn)
    public List<List<Integer>> threeSum2(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<List<Integer>>();

        // 枚举 a
        for (int first = 0; first < n; ++first) {
            // 需要和上一次枚举的数不相同
            if (first > 0 && nums[first] == nums[first - 1])
                continue;

            // a + b + c = 0, b + c = -a = target
            int target = -nums[first];
            // 枚举 b
            for (int second = first + 1; second < n; ++second) {
                // 需要和上一次枚举的数不相同
                if (second > first + 1 && nums[second] == nums[second - 1])
                    continue;

                // 随着 b 后续的增加，不会有满足 a+b+c=0 并且 b<c 的 c 了，可以退出循环
                if (nums[second] > target / 2)
                    break;

                // 需要保证 b 的指针在 c 的指针的左侧
                int third = Arrays.binarySearch(nums, second + 1, n, target - nums[second]);

                // 没找到当前b对应的c，返回的是 -(应当插入的位置)-1
                if (third < 0)
                    continue;

                // third>=0，则找到了对应的c
                // 所得即答案
                List<Integer> list = Arrays.asList(nums[first], nums[second], nums[third]);// （一般还要放入实现类的构造器中，asList返回的是内部类）
                ans.add(list);
            }
        }
        return ans;
    }

    // 17.电话号码的字母组合
    // 给定一个仅包含数字 2-9 的字符串，返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
    // 给出数字到字母的映射如下（与电话按键相同）。注意 1 不对应任何字母。
    // 提示：
    // 0 <= digits.length <= 4
    // digits[i] 是范围 ['2', '9'] 的一个数字。

    // 方法一：dfs 回溯（不存在不可行的解，直接穷举即可）-时间复杂度：O(3^m × 4^n)，空间复杂度：O(m+n)
    public List<String> letterCombinations(String digits) {
        List<String> combinations = new ArrayList<String>();
        if (digits.length() == 0)
            return combinations;

        Map<Character, String> phoneMap = new HashMap<Character, String>() {
            {
                put('2', "abc");
                put('3', "def");
                put('4', "ghi");
                put('5', "jkl");
                put('6', "mno");
                put('7', "pqrs");
                put('8', "tuv");
                put('9', "wxyz");
            }
        };
        backtrack(combinations, phoneMap, digits, 0, new StringBuffer());
        return combinations;
    }

    public void backtrack(List<String> combinations, Map<Character, String> phoneMap, String digits, int index,
            StringBuffer combination) {
        if (index == digits.length()) // 到头了，加入答案List
            combinations.add(combination.toString());
        else {
            char digit = digits.charAt(index);
            String letters = phoneMap.get(digit);
            int lettersCount = letters.length();
            for (int i = 0; i < lettersCount; i++) {
                combination.append(letters.charAt(i));
                backtrack(combinations, phoneMap, digits, index + 1, combination);
                combination.deleteCharAt(index);// 递归完后，删除当前字符
            }
        }
    }

    // 方法二：bfs队列辅助

    // 19.删除链表的倒数第N个结点
    // 给你一个链表，删除链表的倒数第 n 个结点，并且返回链表的头结点。
    // 提示：
    // 链表中结点的数目为 sz
    // 1 <= sz <= 30
    // 0 <= Node.val <= 100
    // 1 <= n <= sz
    // 进阶：你能尝试使用一趟扫描实现吗？

    // 在对链表进行操作时，一种常用的技巧是添加一个哑节点（dummy node），它的 next 指针指向链表的头节点。
    // 这样一来，我们就不需要对头节点进行特殊的判断了。

    // 方法一：计算链表长度（两趟扫描）-时间复杂度：O(L)，空间复杂度：O(1)
    // 首先从头节点开始对链表进行一次遍历，得到链表的长度 L。
    // 随后我们再从头节点开始对链表进行一次遍历，当遍历到第 L−n+1 个节点时，它就是我们需要删除的节点。
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        int length = getLength(head);
        ListNode cur = dummy;
        for (int i = 1; i < length - n + 1; ++i)
            cur = cur.next;

        cur.next = cur.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    public int getLength(ListNode head) {
        int length = 0;
        while (head != null) {
            ++length;
            head = head.next;
        }
        return length;
    }

    // 方法二：栈（一趟扫描）-时间复杂度：O(L)，空间复杂度：O(L)
    // 我们也可以在遍历链表的同时将所有节点依次入栈。
    // 根据栈「先进后出」的原则，我们弹出栈的第 n 个节点就是需要删除的节点，并且目前栈顶的节点就是待删除节点的前驱节点。
    public ListNode removeNthFromEnd2(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        Deque<ListNode> stack = new LinkedList<ListNode>();
        ListNode cur = dummy;
        while (cur != null) {
            stack.push(cur);
            cur = cur.next;
        }
        for (int i = 0; i < n; ++i)
            stack.pop();

        ListNode prev = stack.peek();
        prev.next = prev.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    // 自己写的dfs-时间复杂度：O(L)，空间复杂度：O(L)
    public ListNode removeNthFromEnd22(ListNode head, int n) {
        ListNode dummyHead = new ListNode(-1, head);
        dfs(dummyHead, n);
        return dummyHead.next;
    }

    int dfs(ListNode head, int n) {
        if (head == null)
            return 0;

        int count = dfs(head.next, n);
        if (count == n)
            head.next = head.next.next;
        return count + 1;
    }

    // 方法三：双指针（一趟扫描，且空间复杂度为O(1)）-时间复杂度：O(L)，空间复杂度：O(1)
    // 由于我们需要找到倒数第 n 个节点，因此我们可以使用两个指针 first 和 second 同时对链表进行遍历，
    // 并且 first 比 second 超前 n 个节点。当 first 遍历到链表的末尾时，second 就恰好处于倒数第 n 个节点。
    // 具体地，初始时 first 和 second 均指向头节点。我们首先使用 first 对链表进行遍历，遍历的次数为 n。
    // 此时， first 和 second 之间间隔了 n−1 个节点，即 first 比 second 超前了 n 个节点。
    // 在这之后，我们同时使用 first 和 second 对链表进行遍历。
    // 当 first 遍历到链表的末尾（即 first 为空指针）时，second 恰好指向倒数第 n 个节点。
    public ListNode removeNthFromEnd3(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        ListNode first = head;
        ListNode second = dummy;

        for (int i = 0; i < n; ++i)
            first = first.next;

        while (first != null) {
            first = first.next;
            second = second.next;
        }
        second.next = second.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    // 20.有效的括号
    // 给定一个只包括 '('，')'，'{'，'}'，'['，']' 的字符串 s ，判断字符串是否有效。
    // 有效字符串需满足：
    // 左括号必须用相同类型的右括号闭合。
    // 左括号必须以正确的顺序闭合。
    // 每个右括号都有一个对应的相同类型的左括号。
    // 提示：
    // 1 <= s.length <= 104
    // s 仅由括号 '()[]{}' 组成

    // 方法一：（自己写的）栈-时间复杂度：O(n)，空间复杂度：O(n)
    public boolean isValid(String s) {
        Deque<Character> stk = new ArrayDeque<>();
        Map<Character, Character> map = new HashMap<>() {
            {
                put(']', '[');
                put(')', '(');
                put('}', '{');
            }
        };
        for (char c : s.toCharArray()) {
            if (map.containsKey(c)) {
                if (!map.isEmpty() && stk.peek() == map.get(c))
                    stk.poll();
                else
                    return false;
            } else
                stk.push(c);
        }
        return stk.isEmpty();
    }

    // 21.合并两个有序链表
    // 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
    // 提示：
    // 两个链表的节点数目范围是 [0, 50]
    // -100 <= Node.val <= 100
    // l1 和 l2 均按 非递减顺序 排列

    // 方法一：（自己写的）迭代-时间复杂度：O(m+n)，空间复杂度：O(1)
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        ListNode dummyHead = new ListNode(-1);
        ListNode curr = dummyHead;
        while (list1 != null || list2 != null) {
            int list1Val = list1 == null ? Integer.MAX_VALUE : list1.val;
            int list2Val = list2 == null ? Integer.MAX_VALUE : list2.val;
            if (list1Val < list2Val) {
                curr.next = list1;
                list1 = list1.next;
            } else {
                curr.next = list2;
                list2 = list2.next;
            }
            curr = curr.next;
        }
        return dummyHead.next;
    }

    // 方法二：dfs 递归-时间复杂度：O(m+n)，空间复杂度：O(m+n)
    public ListNode mergeTwoLists2(ListNode l1, ListNode l2) {
        if (l1 == null)
            return l2;
        else if (l2 == null)
            return l1;
        else if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }

    // 22.括号生成
    // 数字 n 代表生成括号的对数，请你设计一个函数，用于能够生成所有可能的并且 有效的 括号组合。
    // 有效括号组合需满足：左括号必须以正确的顺序闭合。
    // 提示：
    // 1 <= n <= 8

    // 方法一：暴力法-时间复杂度：O(2^{2n}n)，空间复杂度：O(n)
    // 我们可以生成所有 2^2n 个 '(' 和 ')' 字符构成的序列，然后我们检查每一个是否有效即可。

    // 方法二：回溯法-时间复杂度：略，空间复杂度O(n)
    // 方法一还有改进的余地：我们可以只在序列仍然保持有效时才添加 '(' or ')'，而不是像 方法一 那样每次添加。
    // 我们可以通过跟踪到目前为止放置的左括号和右括号的数目来做到这一点，
    // 如果左括号数量不大于 nn，我们可以放一个左括号。如果右括号数量小于左括号的数量，我们可以放一个右括号。
    public List<String> generateParenthesis2(int n) {
        List<String> ans = new ArrayList<String>();
        backtrack(ans, new StringBuilder(), 0, 0, n);
        return ans;
    }

    /**
     * 
     * @param ans
     * @param cur
     * @param open  左括号数
     * @param close 右括号数
     * @param max
     */
    public void backtrack(List<String> ans, StringBuilder cur, int open, int close, int max) {
        if (cur.length() == max * 2) {// 长度满足条件
            ans.add(cur.toString());
            return;
        }

        // 还能填左括号
        if (open < max) {
            cur.append('(');
            backtrack(ans, cur, open + 1, close, max);
            cur.deleteCharAt(cur.length() - 1);// 递归后记得删除末尾括号
        }

        // 还能填右括号
        if (close < open) {
            cur.append(')');
            backtrack(ans, cur, open, close + 1, max);
            cur.deleteCharAt(cur.length() - 1);// 递归后记得删除末尾括号
        }
    }

    // 方法三：按括号序列的长度递归-时间复杂度：略，空间复杂度：略
    // 任何一个括号序列都一定是由 ( 开头，并且第一个 ( 一定有一个唯一与之对应的 )。
    // 这样一来，每一个括号序列可以用 (a)b 来表示，其中 a 与 b 分别是一个合法的括号序列（可以为空）。

    class ssoltion_22_3 {
        @SuppressWarnings("unchecked")
        List<String>[] cache = new ArrayList[9];// 记忆化缓存 含有n个括号的所有括号组合，提示：1 <= n <= 8

        public List<String> generate(int n) {
            if (cache[n] != null) // 已经计算过cache[n]
                return cache[n];

            // 计算cache[n]
            ArrayList<String> ans = new ArrayList<String>();
            if (n == 0)
                ans.add("");
            else
                for (int c = 0; c < n; ++c)
                    for (String left : generate(c))
                        for (String right : generate(n - 1 - c))
                            ans.add("(" + left + ")" + right);

            cache[n] = ans;
            return ans;
        }

        public List<String> generateParenthesis3(int n) {
            return generate(n);
        }
    }
    // 23.合并K个升序链表
    // 给你一个链表数组，每个链表都已经按升序排列。
    // 请你将所有链表合并到一个升序链表中，返回合并后的链表。
    // 提示：
    // k == lists.length
    // 0 <= k <= 10^4
    // 0 <= lists[i].length <= 500
    // -10^4 <= lists[i][j] <= 10^4
    // lists[i] 按 升序 排列
    // lists[i].length 的总和不超过 10^4

    // 方法一：（自己写的）优先队列-时间复杂度：O(kn×logk)，空间复杂度：O(k)
    public ListNode mergeKLists(ListNode[] lists) {
        PriorityQueue<ListNode> pq = new PriorityQueue<>((l1, l2) -> l1.val - l2.val);
        ListNode dummyHead = new ListNode(-1);
        ListNode curr = dummyHead;
        for (ListNode list : lists)
            if (list != null)
                pq.offer(list);

        while (!pq.isEmpty()) {
            ListNode min = pq.poll();
            curr.next = min;
            curr = curr.next;
            if (min.next != null)
                pq.offer(min.next);
        }
        return dummyHead.next;
    }

    // 方法二：分治合并-时间复杂度为 O(kn×logk)，空间复杂度：O(logk)
    // 优化方法一，用分治的方法进行合并。（思路：归并排序）
    public ListNode mergeKLists2(ListNode[] lists) {
        return merge(lists, 0, lists.length - 1);
    }

    // l r为链表数组的索引
    public ListNode merge(ListNode[] lists, int l, int r) {
        if (l == r)
            return lists[l];

        if (l > r)
            return null;

        int mid = (l + r) >> 1;
        return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
    }

    // 方法三：顺序合并-时间复杂度为 O(k^2 n)，空间复杂度：O(1)
    // （合并k个升序链表分解为多次合并两个升序链表）
    // 用一个变量 ans 来维护以及合并的链表，第 i 次循环把第 i 个链表和 ans 合并，答案保存到 ans 中。

    // 26.删除有序数组中的重复项
    // 给你一个 升序排列 的数组 nums ，请你 原地 删除重复出现的元素，使每个元素 只出现一次 ，返回删除后数组的新长度。元素的 相对顺序 应该保持一致
    // 由于在某些语言中不能改变数组的长度，所以必须将结果放在数组nums的第一部分。更规范地说，如果在删除重复项之后有 k 个元素，那么 nums 的前 k
    // 个元素应该保存最终结果。
    // 将最终结果插入 nums 的前 k 个位置后返回 k 。
    // 不要使用额外的空间，你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。

    // 方法一：双指针-时间复杂度：O(n)，空间复杂度：O(1)
    public int removeDuplicates(int[] nums) {
        int n = nums.length;
        if (n == 0)
            return 0;

        int fast = 1, slow = 1;
        while (fast < n) {
            if (nums[fast] != nums[fast - 1])
                nums[slow++] = nums[fast];
            fast++;
        }
        return slow;
    }

    // 28.找出字符串中第一个匹配项的下标
    // 给你两个字符串 haystack 和 needle ，
    // 请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标（下标从 0 开始）。
    // 如果 needle 不是 haystack 的一部分，则返回  -1 。
    // 提示：
    // 1 <= haystack.length, needle.length <= 104
    // haystack 和 needle 仅由小写英文字符组成

    // 方法一：暴力匹配-时间复杂度：O(mn)，空间复杂度：O(1)
    public int strStr(String haystack, String needle) {
        int n = haystack.length(), m = needle.length();
        for (int i = 0; i + m <= n; i++) {
            boolean flag = true;
            for (int j = 0; j < m; j++) {
                if (haystack.charAt(i + j) != needle.charAt(j)) {
                    flag = false;
                    break;
                }
            }
            if (flag)
                return i;
        }
        return -1;
    }

    // 方法一：（自己写的）暴力匹配-时间复杂度：O(mn)，空间复杂度：O(1)
    public int strStr11(String haystack, String needle) {
        int stringLength = haystack.length();
        int substringLength = needle.length();
        for (int i = 0; i <= stringLength - substringLength; i++) {
            int j = 0;
            for (; j < substringLength; j++)
                if (haystack.charAt(i + j) != needle.charAt(j))
                    break;

            if (j == substringLength)
                return i;
        }
        return -1;
    }

    // 方法二：Knuth-Morris-Pratt KMP算法
    // Knuth-Morris-Pratt 算法的核心为前缀函数，记作 π(i)，其定义如下：
    // 对于长度为 m 的字符串 s，其前缀函数 π(i)(0≤i<m) 表示 s 的子串 s[0:i] 的最长的相等的真前缀与真后缀的长度。
    // 特别地，如果不存在符合条件的前后缀，那么 π(i)=0。其中真前缀与真后缀的定义为不等于自身的的前缀与后缀。
    // 我们举个例子说明：字符串 aabaaab 的前缀函数值依次为 0,1,0,1,2,2,3。
    // π(0)=0，因为 a 没有真前缀和真后缀，根据规定为 0（可以发现对于任意字符串 π(0)=0 必定成立）；
    // π(1)=1，因为 aa 最长的一对相等的真前后缀为 a，长度为 1；
    // π(2)=0，因为 aab 没有对应真前缀和真后缀，根据规定为 0；
    // π(3)=1，因为 aaba 最长的一对相等的真前后缀为 a，长度为 1；
    // π(4)=2，因为 aabaa 最长的一对相等的真前后缀为 aa，长度为 2；
    // π(5)=2，因为 aabaaa 最长的一对相等的真前后缀为 aa，长度为 2；
    // π(6)=3，因为 aabaaab 最长的一对相等的真前后缀为 aab，长度为 3。
    public int strStr2(String haystack, String needle) {
        int n = haystack.length(), m = needle.length();
        if (m == 0)
            return 0;

        // 求解前缀函数
        int[] pi = new int[m];
        for (int i = 1, j = 0; i < m; i++) {
            while (j > 0 && needle.charAt(i) != needle.charAt(j))
                j = pi[j - 1];

            if (needle.charAt(i) == needle.charAt(j))
                j++;

            pi[i] = j;
        }

        for (int i = 0, j = 0; i < n; i++) {
            while (j > 0 && haystack.charAt(i) != needle.charAt(j))
                j = pi[j - 1];

            if (haystack.charAt(i) == needle.charAt(j))
                j++;

            if (j == m)
                return i - m + 1;
        }
        return -1;
    }

    // 29.两数相除
    // 给你两个整数，被除数 dividend 和除数 divisor。将两数相除，要求 不使用 乘法、除法和取余运算。
    // 整数除法应该向零截断，也就是截去（truncate）其小数部分。例如，8.345 将被截断为 8 ，-2.7335 将被截断至 -2 。
    // 返回被除数 dividend 除以除数 divisor 得到的 商 。
    // 注意：假设我们的环境只能存储 32 位 有符号整数，其数值范围是 [−231,  231 − 1] 。
    // 本题中，如果商 严格大于 231 − 1 ，则返回 231 − 1 ；如果商 严格小于 -231 ，则返回 -231 。
    // 提示：
    // -231 <= dividend, divisor <= 231 - 1
    // divisor != 0

    // 思路：
    // 由于题目规定了「只能存储 32 位整数」，本题解的正文部分和代码中都不会使用任何 64 位整数。
    // 诚然，使用 64 位整数可以极大地方便我们的编码，但这是违反题目规则的。
    // 我们可以考虑将被除数和除数都变为负数，这样就不会有溢出的问题，在编码时只需要考虑 1 种情况了。

    // 方法一：二分查找-时间复杂度：O(log^2 C)，其中 C 表示 32 位整数的范围，空间复杂度：O(1)

    public int divide(int dividend, int divisor) {
        // 考虑被除数为最小值的情况
        if (dividend == Integer.MIN_VALUE) {
            if (divisor == 1)
                return Integer.MIN_VALUE;

            if (divisor == -1)
                return Integer.MAX_VALUE;
        }
        // 考虑除数为最小值的情况
        if (divisor == Integer.MIN_VALUE)
            return dividend == Integer.MIN_VALUE ? 1 : 0;

        // 考虑被除数为 0 的情况
        if (dividend == 0)
            return 0;

        // 一般情况，使用二分查找
        // 将所有的正数取相反数，这样就只需要考虑一种情况
        boolean rev = false;
        if (dividend > 0) {
            dividend = -dividend;
            rev = !rev;
        }
        if (divisor > 0) {
            divisor = -divisor;
            rev = !rev;
        }

        int left = 1, right = Integer.MAX_VALUE, ans = 0;
        while (left <= right) {
            // 注意溢出，并且不能使用除法
            int mid = left + ((right - left) >> 1);
            boolean check = quickAdd(divisor, mid, dividend);
            if (check) {
                ans = mid;
                // 注意溢出
                if (mid == Integer.MAX_VALUE)
                    break;

                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return rev ? -ans : ans;
    }

    // 快速乘
    public boolean quickAdd(int y, int z, int x) {
        // x 和 y 是负数，z 是正数
        // 需要判断 z * y >= x 是否成立
        int result = 0, add = y;
        while (z != 0) {
            if ((z & 1) != 0) {
                // 需要保证 result + add >= x
                if (result < x - add)
                    return false;
                result += add;
            }
            if (z != 1) {
                // 需要保证 add + add >= x
                if (add < x - add)
                    return false;
                add += add;
            }
            // 不能使用除法
            z >>= 1;
        }
        return true;
    }

    // 方法二：类二分查找（逆向快速乘快速幂的思路）-时间复杂度：O(logC)，其中 C 表示 32 位整数的范围，空间复杂度：O(logC)
    public int divide2(int dividend, int divisor) {
        // 考虑被除数为最小值的情况
        if (dividend == Integer.MIN_VALUE) {
            if (divisor == 1)
                return Integer.MIN_VALUE;
            if (divisor == -1)
                return Integer.MAX_VALUE;
        }
        // 考虑除数为最小值的情况
        if (divisor == Integer.MIN_VALUE)
            return dividend == Integer.MIN_VALUE ? 1 : 0;

        // 考虑被除数为 0 的情况
        if (dividend == 0)
            return 0;

        // 一般情况，使用类二分查找
        // 将所有的正数取相反数，这样就只需要考虑一种情况
        boolean rev = false;
        if (dividend > 0) {
            dividend = -dividend;
            rev = !rev;
        }
        if (divisor > 0) {
            divisor = -divisor;
            rev = !rev;
        }

        List<Integer> candidates = new ArrayList<Integer>();
        candidates.add(divisor);
        int index = 0;
        // 注意溢出
        while (candidates.get(index) >= dividend - candidates.get(index)) {
            candidates.add(candidates.get(index) + candidates.get(index));
            ++index;
        }
        int ans = 0;
        for (int i = candidates.size() - 1; i >= 0; --i) {
            if (candidates.get(i) >= dividend) {
                ans += 1 << i;
                dividend -= candidates.get(i);
            }
        }

        return rev ? -ans : ans;
    }

    // 33.搜索旋转排序数组
    // 整数数组 nums 按升序排列，数组中的值 互不相同 。
    // 在传递给函数之前，nums 在预先未知的某个下标 k（0 <= k < nums.length）上进行了 旋转，
    // 使数组变为 [nums[k], nums[k+1], ..., nums[n-1], nums[0], nums[1], ...,
    // nums[k-1]]（下标 从 0 开始 计数）。
    // 例如， [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。
    // 给你 旋转后 的数组 nums 和一个整数 target ，如果 nums 中存在这个目标值 target ，则返回它的下标，否则返回 -1 。
    // 提示：
    // 1 <= nums.length <= 5000
    // -10^4 <= nums[i] <= 10^4
    // nums 中的每个值都 独一无二
    // 题目数据保证 nums 在预先未知的某个下标上进行了旋转
    // -10^4 <= target <= 10^4
    // 进阶：你可以设计一个时间复杂度为 O(log n) 的解决方案吗？
    // 方法一：（自己写的，完整情况讨论，冗余但易读）二分查找-时间复杂度：O(logn)，空间复杂度： O(1)
    public int search11(int[] nums, int target) {
        int n = nums.length;
        int left = 0;
        int right = n - 1;
        int firstVal = nums[0];
        // e.g. 4 5 6(t) 7 1 2(t) 3
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] == target)
                return mid;
            else if (nums[mid] > target) {// 当前值大于target
                if (target >= firstVal) // target在左（当前值大于target），此时情况唯一
                    right = mid - 1;
                else {// target在右边（当前值大于target）
                    if (nums[mid] < firstVal)// 当前值在左边（target在右边，当前值大于target）
                        right = mid - 1;
                    else // 当前值在右边（target在右边，当前值大于target）
                        left = mid + 1;
                }
            } else {// 当前值小于target
                if (target >= firstVal) {// target在左边（当前值小于target）
                    if (nums[mid] < firstVal)// 当前值在左边（target在左边，当前值小于target）
                        right = mid - 1;
                    else // 当前值在左边（target在左边，当前值小于target）
                        left = mid + 1;
                } else // target在右边（当前值小于target），此时情况唯一
                    left = mid + 1;
            }
        }
        return -1;
    }

    // 34.在排序数组中查找元素的第一个和最后一个位置
    // 给定一个按照「升序排列」的整数数组 nums，和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。
    // 如果数组中不存在目标值 target，返回 [-1, -1]。
    // 进阶：
    // 你可以设计并实现时间复杂度为 O(log n) 的算法解决此问题吗？

    // 方法一：二分查找-时间复杂度： O(logn) ，空间复杂度：O(1)
    // 寻找 leftIdx 即为在数组中寻找第一个大于 target-1 的数的下标，
    // 寻找 rightIdx 即为在数组中寻找第一个大于 target 的下标，然后将下标减一。
    // 跟一般的二分查找不一样的地方在于：本问题找到target时不返回，只有left = right时才返回
    public int[] searchRange(int[] nums, int target) {
        int leftIdx = searchFirstLarger(nums, target - 1);
        int rightIdx = searchFirstLarger(nums, target) - 1;
        // int leftIdx = searchFirstSmaller(nums, target) + 1;
        // int rightIdx = searchFirstSmaller(nums, target + 1);
        if (leftIdx <= rightIdx && nums[leftIdx] == target)
            return new int[] { leftIdx, rightIdx };

        return new int[] { -1, -1 };
    }

    // 第一个大于 target 的数的下标
    private int searchFirstLarger(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] > target)
                right = mid - 1;
            else// nums[mid] <= target时，丢弃左半边
                left = mid + 1;
        }
        return left;
    }

    // 第一个小于 target 的数的下标
    @SuppressWarnings("unused")
    private int searchFirstSmaller(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] >= target)
                right = mid - 1;
            else
                left = mid + 1;
        }
        return right;
    }

    // 36.有效的数独
    // 请你判断一个 9 x 9 的数独是否有效。只需要 根据以下规则 ，验证已经填入的数字是否有效即可。
    // 数字 1-9 在每一行只能出现一次。
    // 数字 1-9 在每一列只能出现一次。
    // 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。（请参考示例图）
    // 注意：
    // 一个有效的数独（部分已被填充）不一定是可解的。
    // 只需要根据以上规则，验证已经填入的数字是否有效即可。
    // 空白格用 '.' 表示。
    // 提示：
    // board.length == 9
    // board[i].length == 9
    // board[i][j] 是一位数字（1-9）或者 '.'

    // 方法一：一次遍历-时间复杂度：O(1)，空间复杂度：O(1)
    public boolean isValidSudoku(char[][] board) {
        int[][] rows = new int[9][9];
        int[][] columns = new int[9][9];
        int[][][] subboxes = new int[3][3][9];
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char c = board[i][j];
                if (c != '.') {
                    int index = c - '0' - 1;
                    rows[i][index]++;
                    columns[j][index]++;
                    subboxes[i / 3][j / 3][index]++;
                    if (rows[i][index] > 1 || columns[j][index] > 1 || subboxes[i / 3][j / 3][index] > 1)
                        return false;
                }
            }
        }
        return true;
    }

    // 方法一：（自己写的）一次遍历-时间复杂度：O(1)，空间复杂度：O(1)
    public boolean isValidSudoku11(char[][] board) {
        boolean[][] rows = new boolean[9][9];
        boolean[][] columns = new boolean[9][9];
        boolean[][][] boxs = new boolean[3][3][9];
        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                char c = board[i][j];
                if (c == '.')
                    continue;
                int num = c - '1';
                if (rows[i][num] || columns[j][num] || boxs[i / 3][j / 3][num])
                    return false;
                rows[i][num] = true;
                columns[j][num] = true;
                boxs[i / 3][j / 3][num] = true;
            }
        }
        return true;
    }

    // 38.外观数列
    // 给定一个正整数 n ，输出外观数列的第 n 项。
    // 「外观数列」是一个整数序列，从数字 1 开始，序列中的每一项都是对前一项的描述。
    // 你可以将其视作是由递归公式定义的数字字符串序列：
    // countAndSay(1) = "1"
    // countAndSay(n) 是对 countAndSay(n-1) 的描述，然后转换成另一个数字字符串。
    // 前五项如下：
    // 1. 1
    // 2. 11
    // 3. 21
    // 4. 1211
    // 5. 111221
    // 第一项是数字 1
    // 描述前一项，这个数是 1 即 “ 一 个 1 ”，记作 "11"
    // 描述前一项，这个数是 11 即 “ 二 个 1 ” ，记作 "21"
    // 描述前一项，这个数是 21 即 “ 一 个 2 + 一 个 1 ” ，记作 "1211"
    // 描述前一项，这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ，记作 "111221"
    // 要 描述 一个数字字符串，首先要将字符串分割为 最小 数量的组，每个组都由连续的最多 相同字符 组成。
    // 然后对于每个组，先描述字符的数量，然后描述字符，形成一个描述组。
    // 要将描述转换为数字字符串，先将每组中的字符数量用数字替换，再将所有描述组连接起来。
    // 提示：
    // 1 <= n <= 30

    // 方法一：遍历生成-时间复杂度：O(mn)，空间复杂度：O(m)
    public String countAndSay(int n) {
        String str = "1";
        for (int i = 2; i <= n; ++i) {
            StringBuilder sb = new StringBuilder();
            int start = 0;
            int pos = 0;

            while (pos < str.length()) {
                while (pos < str.length() && str.charAt(pos) == str.charAt(start))
                    pos++;

                sb.append(Integer.toString(pos - start)).append(str.charAt(start));
                start = pos;
            }
            str = sb.toString();
        }
        return str;
    }

    // 方法一：（自己写的）遍历生成-时间复杂度：O(mn)，空间复杂度：O(m)
    public String countAndSay11(int n) {
        StringBuilder prev = new StringBuilder("1");
        StringBuilder curr = new StringBuilder("1");
        while (n > 1) {
            n--;
            curr = new StringBuilder();
            int count = 0;
            char c = prev.charAt(0);
            for (int i = 0; i < prev.length(); i++) {
                if (prev.charAt(i) == c)
                    count++;
                else {
                    curr.append(count).append(c);
                    count = 1;
                    c = prev.charAt(i);
                }
            }
            curr.append(count).append(c);
            prev = curr;
        }
        return curr.toString();
    }

    // 方法二：打表

    // 41.缺失的第一个正数
    // 给你一个未排序的整数数组 nums ，请你找出其中没有出现的最小的正整数。
    // 请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
    // 提示：
    // 1 <= nums.length <= 5 * 105
    // -231 <= nums[i] <= 231 - 1

    // 「真正」满足时间复杂度为 O(N) 且空间复杂度为 O(1) 的算法是不存在的
    // 但是我们可以退而求其次：利用给定数组中的空间来存储一些状态。

    // 方法一：哈希表-时间复杂度：O(n)，空间复杂度：O(1)
    // 数组索引：0, 1, 2,... , n-1
    // 记录数值：1, 2, 3,... , n
    public int firstMissingPositive(int[] nums) {
        int n = nums.length;

        // 1. 预处理，只有处于范围[1, n]之间的数需要处理
        for (int i = 0; i < n; ++i)
            if (nums[i] <= 0)
                nums[i] = n + 1;// 处理为 n+1 ，后续不用处理

        // 2. 利用给定数组中的空间来存储一些状态
        for (int i = 0; i < n; ++i) {
            int num = Math.abs(nums[i]);
            // 初始值 > n 的不用处理
            if (num <= n)
                nums[num - 1] = -Math.abs(nums[num - 1]);
        }

        // 3. 根据数组中记录的状态，求得最终答案
        for (int i = 0; i < n; ++i)
            if (nums[i] > 0)
                return i + 1;

        return n + 1;
    }

    // 方法一：（自己写的）哈希表-时间复杂度：O(n)，空间复杂度：O(1)
    // 数组索引：0, 1, 2,... , n-1
    // 记录数值：1, 2, 3,... , n
    public int firstMissingPositive11(int[] nums) {
        int n = nums.length;
        // 1. 预处理，只有处于范围[1, n]之间的数需要处理
        for (int i = 0; i < n; i++)
            if (nums[i] < 1 || n < nums[i])
                nums[i] = n + 1;// 处理为 n+1 ，后续不用处理

        // 2. 利用给定数组中的空间来存储一些状态
        for (int i = 0; i < n; i++) {
            if (nums[i] == n + 1)
                continue;

            int index = 0;
            if (nums[i] < 0)// 2.1. 需要先还原值，再求对应索引
                index = nums[i] + n + 1 - 1;
            else if (nums[i] > 0)// 2.2. 直接求对应索引
                index = nums[i] - 1;
            else// 2.3. nums[i]==0，还原后为n+1，不用处理
                continue;

            if (nums[index] > 0)
                nums[index] -= n + 1;
        }

        // 3. 根据数组中记录的状态，求得最终答案
        for (int i = 0; i < n; i++)
            if (nums[i] > 0)
                return i + 1;

        return n + 1;
    }

    // 方法二：置换-时间复杂度：O(n)，空间复杂度：O(1)
    // 数组索引：0, 1, 2,... , n-1
    // 期望数值：1, 2, 3,... , n
    // 由于每次的交换操作都会使得某一个数交换到正确的位置，因此交换的次数最多为 N，整个方法的时间复杂度为 O(N)。
    public int firstMissingPositive2(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; ++i) {
            // 处于范围[1, n]的数才有置换的必要，避免原地置换出现死循环
            while (nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
                int temp = nums[nums[i] - 1];
                nums[nums[i] - 1] = nums[i];
                nums[i] = temp;
            }
        }

        // 是期望数值则有该数，不是则表示缺失该数
        for (int i = 0; i < n; ++i)
            if (nums[i] != i + 1)
                return i + 1;

        return n + 1;
    }

    // 42.接雨水
    // 给定 n 个非负整数表示每个宽度为 1 的柱子的高度图，计算按此排列的柱子，下雨之后能接多少雨水
    // n == height.length
    // 0 <= n <= 3e4
    // 0 <= height[i] <= 1e5

    // 方法一：动态规划 保存左右最高位-时间复杂度：O(n)，空间复杂度：O(n)
    // 下标 i 处能接的雨水量 =
    // 下标 i 处的水能到达的最大高度（下标 i 两边的最大高度的最小值）- height[i]
    // 创建两个长度为 n 的数组 leftMax 和 rightMax
    // 对于 1 ≤ i < n-1 ，leftMax[i] 表示下标 i 及其左边的位置中，height 的最大高度
    // rightMax[i] 表示下标 i 及其右边的位置中，height 的最大高度
    public int trap(int[] height) {
        int n = height.length;
        if (n == 0)
            return 0;

        int[] leftMax = new int[n];
        leftMax[0] = height[0];// 最左
        for (int i = 1; i < n; ++i)
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);

        int[] rightMax = new int[n];
        rightMax[n - 1] = height[n - 1];// 最右
        for (int i = n - 2; i >= 0; --i)
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);

        int ans = 0;
        for (int i = 0; i < n; ++i)
            ans += Math.min(leftMax[i], rightMax[i]) - height[i];

        return ans;
    }

    // 方法二：单调栈-时间复杂度：O(n)，空间复杂度：O(n)
    // 维护一个单调栈，单调栈存储的是下标
    // 满足从栈底到栈顶的下标对应的数组 height 中的元素递减（凹槽左边界）
    public int trap2(int[] height) {
        int ans = 0;
        Deque<Integer> stack = new LinkedList<Integer>();// 单调栈存储的是下标
        int n = height.length;
        for (int i = 0; i < n; ++i) {
            // 栈非空，或当前元素（凹槽右边界） > 栈顶元素
            while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
                // 单调栈的特性保证栈顶2个元素分别是槽底和左边界
                int top = stack.pop();// 栈顶元素出栈（槽底）
                if (stack.isEmpty()) // 栈内唯一元素出栈
                    break;
                int left = stack.peek();// 凹槽左边界
                int currWidth = i - left - 1;// 凹槽宽度
                // 左右边界较小值-槽底
                int currHeight = Math.min(height[left], height[i]) - height[top];
                ans += currWidth * currHeight;
            }
            stack.push(i);
        }
        return ans;
    }

    // 方法三：双指针-时间复杂度：O(n)，空间复杂度：O(1)
    // 维护两个指针 left 和 right，以及两个变量 leftMax 和 rightMax，
    // 初始时 left = 0, right = n−1, leftMax = 0, rightMax = 0
    // 指针 left 只会向右移动，指针 right 只会向左移动
    // 移动指针的过程中维护两个变量 leftMax 和 rightMax 的值
    // 当两个指针没有相遇时，进行如下操作：
    // 使用 height[left] 和 height[right] 的值更新 leftMax 和 rightMax 的值
    // 1.如果 height[left] < height[right] ，则必有 leftMax < rightMax
    // 下标 left 处能接的雨水量等于 leftMax−height[left]
    // 将下标 left 处能接的雨水量加到能接的雨水总量，然后将 left 加 1（即向右移动一位）
    // 2.如果 height[left] ≥ height[right] ，则必有leftMax ≥ rightMax
    // 下标 right 处能接的雨水量等于 rightMax−height[right]
    // 将下标 right 处能接的雨水量加到能接的雨水总量，然后将 right 减 1（即向左移动一位）
    // 当两个指针相遇时，即可得到能接的雨水总量
    public int trap3(int[] height) {
        int ans = 0;
        int left = 0, right = height.length - 1;// 最左/最右下标
        int leftMax = 0, rightMax = 0;
        while (left < right) {// 左右指针相遇，跳出
            leftMax = Math.max(leftMax, height[left]);
            rightMax = Math.max(rightMax, height[right]);// 每次循环只有一边最大边界得到更新
            // left right指针会停留在leftmax rightmax所在位置，直到另一个超过自己
            // 指针移动时，一定伴随着ans的更新，因此要确保可能存在的更高边界（max等待被超）
            if (height[left] < height[right]) {
                ans += leftMax - height[left];// （更小边界-底）
                ++left;// 更小一边移动
            } else {
                ans += rightMax - height[right];// （更小边界-底）
                --right;// 更小一边移动
            }
        }
        return ans;
    }

    // 44.通配符匹配
    // 给定一个字符串 (s) 和一个字符模式 (p) ，实现一个支持 '?' 和 '*' 的通配符匹配。
    // '?' 可以匹配任何单个字符。
    // '*' 可以匹配任意字符串（包括空字符串）。
    // 两个字符串完全匹配才算匹配成功。
    // 说明:
    // s 可能为空，且只包含从 a-z 的小写字母。
    // p 可能为空，且只包含从 a-z 的小写字母，以及字符 ? 和 *。

    // 本题与「10. 正则表达式匹配」非常类似，但相比较而言，本题稍微容易一些。
    // 因为在本题中，模式 p 中的任意一个字符都是独立的，即不会和前后的字符互相关联，形成一个新的匹配模式。
    // 因此，本题的状态转移方程需要考虑的情况会少一些。

    // 方法一：动态规划-时间复杂度：O(mn)，空间复杂度：O(mn)
    public boolean isMatch44(String s, String p) {
        int m = s.length(), n = p.length();
        boolean[][] dp = new boolean[m + 1][n + 1];

        dp[0][0] = true;
        for (int i = 1; i <= n; ++i) {
            if (p.charAt(i - 1) == '*')
                dp[0][i] = true;
            else
                break;
        }

        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                if (p.charAt(j - 1) == '*')
                    dp[i][j] = dp[i][j - 1] || dp[i - 1][j];
                else if (p.charAt(j - 1) == '?' || s.charAt(i - 1) == p.charAt(j - 1))
                    dp[i][j] = dp[i - 1][j - 1];
            }
        }
        return dp[m][n];
    }

    // 方法二：贪心算法-时间复杂度：O(mn)，空间复杂度：O(1)
    // 方法一的瓶颈在于对星号 * 的处理方式：使用动态规划枚举所有的情况。
    // 由于星号是「万能」的匹配字符，连续的多个星号和单个星号实际上是等价的，那么不连续的多个星号呢？

    // 我们以 p=∗ abcd ∗ 为例，p 可以匹配所有包含子串 abcd 的字符串，
    // 也就是说，我们只需要暴力地枚举字符串 s 中的每个位置作为起始位置，并判断对应的子串是否为 abcd 即可。
    // 这种暴力方法的时间复杂度为 O(mn)，与动态规划一致，但不需要额外的空间。

    // 如果 p=∗ abcd∗efgh∗i∗ 呢？显然，p 可以匹配所有依次出现子串 abcd、efgh、i 的字符串。
    // 此时，对于任意一个字符串 s，我们首先暴力找到最早出现的 abcd，随后从下一个位置开始暴力找到最早出现的 efgh，
    // 最后找出 i，就可以判断 s 是否可以与 p 匹配。
    // 这样「贪心地」找到最早出现的子串是比较直观的，
    // 因为如果 s 中多次出现了某个子串，那么我们选择最早出现的位置，可以使得后续子串能被找到的机会更大。

    public boolean isMatch44_2(String s, String p) {
        int sRight = s.length(), pRight = p.length();

        // 1. 如果模式 p 的结尾字符不是星号，那么就必须与字符串 s 的结尾字符匹配。
        // 那么我们不断地匹配 s 和 p 的结尾字符，直到 p 为空或者 p 的结尾字符是星号为止。
        // 在这个过程中，如果匹配失败，或者最后 p 为空但 s 不为空，那么需要返回 False。
        while (sRight > 0 && pRight > 0 && p.charAt(pRight - 1) != '*') {
            if (charMatch(s.charAt(sRight - 1), p.charAt(pRight - 1))) {
                --sRight;
                --pRight;
            } else
                return false;
        }
        // 退出循环时，pRight 一定指向末尾*

        // p 中没有*，模式 p 的开头字符不是星号，我们可以不断地匹配 s 和 p 的开头字符。
        if (pRight == 0)
            return sRight == 0;

        int sIndex = 0, pIndex = 0;

        // 记录 p 出现 * 时 sIndex pIndex 的位置，sRecord 和 tRecord 的初始值为 -1，表示模式 p 的开头字符不是星号
        int sRecord = -1, pRecord = -1;

        while (sIndex < sRight && pIndex < pRight) {
            // 记录 p 出现 * 时 sIndex pIndex 的位置
            if (p.charAt(pIndex) == '*') {
                ++pIndex;
                sRecord = sIndex;
                pRecord = pIndex;

                // 当前字符匹配成功
            } else if (charMatch(s.charAt(sIndex), p.charAt(pIndex))) {
                ++sIndex;
                ++pIndex;

                // 当前字符匹配失败
                // 把当前匹配失败的字符并入前面遇到的*中，sIndex pIndex 退回记录点，重新开始匹配当前子串（贪心）
            } else if (sRecord != -1) {// 有「反悔」重新进行匹配的机会。
                ++sRecord;
                sIndex = sRecord;
                pIndex = pRecord;
            } else// sRecord 仍然为 -1，说明没有「反悔」重新进行匹配的机会。
                return false;
        }

        for (int i = pIndex; i < pRight; ++i)
            if (p.charAt(i) != '*')
                return false;
        return true;
    }

    public boolean charMatch(char u, char v) {
        return u == v || v == '?';
    }

    // 46.全排列
    // 给定一个不含重复数字的数组 nums ，返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
    // 提示：
    // 1 <= nums.length <= 6
    // -10 <= nums[i] <= 10
    // nums 中的所有整数 互不相同

    // 方法一：回溯 + 交换-时间复杂度：O(n×n!)，空间复杂度：O(n)
    // 核心思想：依次交换位置
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    List<Integer> output = new ArrayList<Integer>();
    int n;

    public List<List<Integer>> permute(int[] nums) {
        for (int num : nums)
            output.add(num);

        n = nums.length;
        backtrack(0);
        return res;
    }

    // 从左往右填到第 first 个位置
    public void backtrack(int first) {
        // 所有数都填完了
        if (first == n)
            res.add(new ArrayList<Integer>(output));

        for (int i = first; i < n; i++) {
            // 动态维护数组
            Collections.swap(output, first, i);// 第一次循环first = i，不交换
            // 继续递归填下一个数
            backtrack(first + 1);
            // 撤销操作
            Collections.swap(output, first, i);
        }
    }

    // 方法二：（自己写的）dfs 回溯-时间复杂度：O(n×n!)，空间复杂度：O(n)
    class tsolution46 {
        List<List<Integer>> res = new ArrayList<>();
        List<Integer> ans = new ArrayList<>();
        int n;
        int[] nums;
        boolean[] hasVisited;

        public List<List<Integer>> permute(int[] nums) {
            this.n = nums.length;
            this.nums = nums;
            this.hasVisited = new boolean[n];
            dfs(0);
            return res;
        }

        private void dfs(int index) {
            if (index == n) {
                res.add(new ArrayList<>(ans));
                return;
            }
            for (int i = 0; i < n; i++) {
                if (hasVisited[i])
                    continue;

                hasVisited[i] = true;
                ans.add(nums[i]);

                dfs(index + 1);

                hasVisited[i] = false;
                ans.remove(ans.size() - 1);
            }
        }
    }

    // 48.旋转图像
    // 给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
    // 你必须在 原地 旋转图像，这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。
    // 提示：
    // matrix.length == n
    // matrix[i].length == n
    // 1 <= n <= 20
    // -1000 <= matrix[i][j] <= 1000

    // 方法一：原地旋转-时间复杂度：O(n^2)，空间复杂度：O(1)
    // 四数交换为一组
    public void rotate(int[][] matrix) {
        int n = matrix.length;
        for (int i = 0; i < n / 2; ++i)
            for (int j = 0; j < (n + 1) / 2; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n - j - 1][i];
                matrix[n - j - 1][i] = matrix[n - i - 1][n - j - 1];
                matrix[n - i - 1][n - j - 1] = matrix[j][n - i - 1];
                matrix[j][n - i - 1] = temp;
            }

    }

    // 方法二：用翻转代替旋转-时间复杂度：O(n^2)，空间复杂度：O(1)
    // 先将其通过水平轴翻转，再根据主对角线翻转，得到答案
    // 先主对角线翻转，则后垂直翻转
    public void rotate2(int[][] matrix) {
        int n = matrix.length;
        // 水平翻转
        for (int i = 0; i < n / 2; ++i)
            for (int j = 0; j < n; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[n - i - 1][j];
                matrix[n - i - 1][j] = temp;
            }

        // 主对角线翻转
        for (int i = 0; i < n; ++i)
            for (int j = 0; j < i; ++j) {
                int temp = matrix[i][j];
                matrix[i][j] = matrix[j][i];
                matrix[j][i] = temp;
            }
    }

    // 49.字母异位词分组
    // 给你一个字符串数组，请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
    // 字母异位词 是由重新排列源单词的字母得到的一个新单词，所有源单词中的字母都恰好只用一次。
    // 提示：
    // 1 <= strs.length <= 104
    // 0 <= strs[i].length <= 100
    // strs[i] 仅包含小写字母

    // 本题关键在于用什么作键（键尽可能短）
    // 引用数据类型当键，必须是地址相同才行（equal()逻辑），String除外

    // 方法一：排序（排序之后的字符串作为哈希表的键）-时间复杂度：O(nklogk)，空间复杂度：O(nk)
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<String, List<String>>();
        for (String str : strs) {
            char[] array = str.toCharArray();
            Arrays.sort(array);
            String key = new String(array);
            List<String> list = map.getOrDefault(key, new ArrayList<String>());
            list.add(str);
            map.put(key, list);
        }
        return new ArrayList<>(map.values());
    }

    // 方法二：计数（出现次数大于 0 的字母和出现次数按顺序拼接成字符串，作为哈希表的键）
    // 时间复杂度：O(n(k+∣Σ∣))，空间复杂度：O(n(k+∣Σ∣))
    public List<List<String>> groupAnagrams2(String[] strs) {
        Map<String, List<String>> map = new HashMap<String, List<String>>();
        for (String str : strs) {
            int[] counts = new int[26];
            int length = str.length();
            for (int i = 0; i < length; i++)
                counts[str.charAt(i) - 'a']++;

            // 将每个出现次数大于 0 的字母和出现次数按顺序拼接成字符串，作为哈希表的键
            // 不能只用出现次数拼接， 0 10， 0 1 0 无法区分
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < 26; i++)
                if (counts[i] != 0) {// 精髓所在：只拼接非0次数，尽可能减少字符键的长度！
                    sb.append((char) ('a' + i));
                    sb.append(counts[i]);
                }

            String key = sb.toString();// 精髓所在：String重写了equals，比较的是内容，StringBuilder没有重写equals，比较的是地址
            List<String> list = map.getOrDefault(key, new ArrayList<String>());
            list.add(str);
            map.put(key, list);
        }
        return new ArrayList<List<String>>(map.values());
    }

    // 方法三：自己的lowb想法 并查集（字母异位词可找到共键，直接哈希表.values()转List得到答案）

    // 50.Pow(x, n)
    // 实现 pow(x, n) ，即计算 x 的整数 n 次幂函数（即，xn ）。
    // 提示：
    // -100.0 < x < 100.0
    // -231 <= n <= 231-1
    // n 是一个整数
    // -104 <= xn <= 104

    // 方法一：快速幂 + 递归-时间复杂度：O(logn)，空间复杂度：O(logn)
    class ssoltion_50 {
        public double myPow(double x, int n) {
            long N = n;
            return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
        }

        public double quickMul(double x, long N) {
            if (N == 0)
                return 1.0;

            double y = quickMul(x, N / 2);
            return N % 2 == 0 ? y * y : y * y * x;
        }
    }

    // 方法二：快速幂 + 迭代-时间复杂度：O(logn)，空间复杂度：O(1)
    public double myPow(double x, int n) {
        long N = n;
        return N >= 0 ? quickMul(x, N) : 1.0 / quickMul(x, -N);
    }

    public double quickMul(double x, long N) {
        double ans = 1.0;
        // 贡献的初始值为 x
        double x_contribute = x;
        // 在对 N 进行二进制拆分的同时计算答案
        while (N > 0) {
            if (N % 2 == 1) {
                // 如果 N 二进制表示的最低位为 1，那么需要计入贡献
                ans *= x_contribute;
            }
            // 将贡献不断地平方
            x_contribute *= x_contribute;
            // 舍弃 N 二进制表示的最低位，这样我们每次只要判断最低位即可
            N /= 2;
        }
        return ans;
    }

    // 53.最大子数组和
    // 给你一个整数数组 nums ，请你找出一个具有最大和的连续子数组（子数组最少包含一个元素），返回其最大和。
    // 子数组 是数组中的一个连续部分。
    // 提示：
    // 1 <= nums.length <= 105
    // -104 <= nums[i] <= 104

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(1)
    public int maxSubArray(int[] nums) {
        int pre = 0, maxAns = nums[0];
        for (int x : nums) {
            pre = Math.max(pre + x, x);
            maxAns = Math.max(maxAns, pre);
        }
        return maxAns;
    }

    // 方法二：（自己写的）分治（线段树）-时间复杂度：O(n)，空间复杂度：O(logn)
    public int maxSubArray2(int[] nums) {
        int[] res = dfs(0, nums.length - 1, nums);
        return Math.max(res[0], Math.max(res[1], res[2]));
    }

    // [leftMax, rightMax, innerMax, sum]
    private int[] dfs(int left, int right, int[] nums) {
        if (left == right)
            return new int[] { nums[left], nums[left], nums[left], nums[left] };

        int mid = (left + right) / 2;
        int[] leftRes = dfs(left, mid, nums);
        int[] rightRes = dfs(mid + 1, right, nums);

        int newLeftMax = Math.max(leftRes[0], Math.max(leftRes[3], leftRes[3] + rightRes[0]));
        int newRightMax = Math.max(rightRes[1], Math.max(rightRes[3], rightRes[3] + leftRes[1]));
        int newInnerMax = Math.max(leftRes[2], Math.max(rightRes[2], leftRes[1] + rightRes[0]));
        int newSum = leftRes[3] + rightRes[3];
        return new int[] { newLeftMax, newRightMax, newInnerMax, newSum };
    }

    // 54.螺旋矩阵
    // 给你一个 m 行 n 列的矩阵 matrix ，请按照 顺时针螺旋顺序 ，返回矩阵中的所有元素。
    // 提示：
    // m == matrix.length
    // n == matrix[i].length
    // 1 <= m, n <= 10
    // -100 <= matrix[i][j] <= 100

    // 方法一：（设置移动方向）模拟-时间复杂度：O(mn)，空间复杂度：O(mn)
    // 可以模拟螺旋矩阵的路径。初始位置是矩阵的左上角，初始方向是向右，当路径超出界限或者进入之前访问过的位置时，顺时针旋转，进入下一个方向。
    // 判断路径是否进入之前访问过的位置需要使用一个与输入矩阵大小相同的辅助矩阵 visited，其中的每个元素表示该位置是否被访问过。
    // 当一个元素被访问时，将 visited 中的对应位置的元素设为已访问。
    // 如何判断路径是否结束？由于矩阵中的每个元素都被访问一次，因此路径的长度即为矩阵中的元素数量，当路径的长度达到矩阵中的元素数量时即为完整路径，将该路径返回。
    public List<Integer> spiralOrder(int[][] matrix) {
        List<Integer> order = new ArrayList<Integer>();
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
            return order;

        int rows = matrix.length, columns = matrix[0].length;
        boolean[][] visited = new boolean[rows][columns];
        int total = rows * columns;
        int row = 0, column = 0;
        int[][] directions = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } };
        int directionIndex = 0;
        for (int i = 0; i < total; i++) {
            order.add(matrix[row][column]);
            visited[row][column] = true;
            int nextRow = row + directions[directionIndex][0], nextColumn = column + directions[directionIndex][1];
            if (nextRow < 0 || nextRow >= rows || nextColumn < 0 || nextColumn >= columns
                    || visited[nextRow][nextColumn])
                directionIndex = (directionIndex + 1) % 4;

            row += directions[directionIndex][0];
            column += directions[directionIndex][1];
        }
        return order;
    }

    // 方法二：按层模拟-时间复杂度：O(mn)，空间复杂度：O(1)
    public List<Integer> spiralOrder2(int[][] matrix) {
        List<Integer> order = new ArrayList<Integer>();
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
            return order;

        int rows = matrix.length, columns = matrix[0].length;
        int left = 0, right = columns - 1, top = 0, bottom = rows - 1;
        while (left <= right && top <= bottom) {
            for (int column = left; column <= right; column++)
                order.add(matrix[top][column]);

            for (int row = top + 1; row <= bottom; row++)
                order.add(matrix[row][right]);

            if (left < right && top < bottom) {
                for (int column = right - 1; column > left; column--)
                    order.add(matrix[bottom][column]);
                for (int row = bottom; row > top; row--)
                    order.add(matrix[row][left]);
            }
            left++;
            right--;
            top++;
            bottom--;
        }
        return order;
    }

    // 方法二：（自己写的）按层模拟-时间复杂度：O(mn)，空间复杂度：O(1)
    public List<Integer> spiralOrder22(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int upRow = 0, downRow = m - 1;
        int leftColumn = 0, rightColumn = n - 1;
        List<Integer> res = new ArrayList<>();
        while (leftColumn <= rightColumn && upRow <= downRow) {
            for (int j = leftColumn; j <= rightColumn; j++)
                res.add(matrix[upRow][j]);

            for (int i = upRow + 1; i < downRow; i++)
                res.add(matrix[i][rightColumn]);

            if (upRow == downRow)
                break;
            for (int j = rightColumn; j >= leftColumn; j--)
                res.add(matrix[downRow][j]);

            if (leftColumn == rightColumn)
                break;
            for (int i = downRow - 1; i > upRow; i--)
                res.add(matrix[i][leftColumn]);

            upRow++;
            downRow--;
            leftColumn++;
            rightColumn--;
        }
        return res;
    }

    // 55.跳跃游戏
    // 给定一个非负整数数组 nums ，你最初位于数组的第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度（可跳范围是一个区间）
    // 判断你是否能够到达最后一个下标（审题！并没有让你求到达的路径）

    // 方法一：贪心-时间复杂度O(n)
    public boolean canJump(int[] nums) {
        int n = nums.length;
        int rightMost = 0;// 最右可达
        for (int i = 0; i < n; ++i) {
            if (i <= rightMost) {// 下标i可达，才可计算下一跳的最右可达
                rightMost = Math.max(rightMost, i + nums[i]);
                if (rightMost >= n - 1)// 最右可达大于最后一个下标，则能够到达
                    return true;
            } // 提前跳出省略的时间也微乎其微
        }
        return false;
    }

    // 56.合并区间
    // 以数组 intervals 表示若干个区间的集合，其中单个区间为 intervals[i] = [starti, endi] 。
    // 请你合并所有重叠的区间，并返回一个不重叠的区间数组，该数组需恰好覆盖输入中的所有区间。
    // 示例 1：
    // 输入：intervals = [[1,3],[2,6],[8,10],[15,18]] （数组不一定按大小顺序排列）
    // 输出：[[1,6],[8,10],[15,18]]
    // 解释：区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]
    // 提示：
    // 1 <= intervals.length <= 104
    // intervals[i].length == 2
    // 0 <= starti <= endi <= 104

    // 方法一：排序 + 栈-时间复杂度：O(nlogn)，空间复杂度：O(logn)
    public int[][] merge(int[][] intervals) {
        if (intervals.length == 0)
            return new int[0][2];

        Comparator<int[]> c = (interval1, interval2) -> interval1[0] - interval2[0];// 以start升序排列

        Arrays.sort(intervals, c);
        // 用数组 merged 存储最终的答案
        Deque<int[]> merged = new LinkedList<int[]>();
        for (int i = 0; i < intervals.length; ++i) {
            int L = intervals[i][0], R = intervals[i][1];
            // merged数组中还无数组，直接加入（初始情况）；或者merged末尾二元组的R小于L（不能合并），加入
            if (merged.size() == 0 || merged.peek()[1] < L)
                merged.push(new int[] { L, R });
            else {// merged数组末尾大于L（能合并），更新merged末尾二元组的R
                int[] lastInterval = merged.pop();
                merged.push(new int[] { lastInterval[0], Math.max(lastInterval[1], R) });
            }
        }
        return merged.toArray(new int[merged.size()][]);
    }

    // 62.不同路径
    // 一个机器人位于一个 m x n 网格的左上角 （起始点在下图中标记为 “Start” ）。
    // 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角（在下图中标记为 “Finish” ）。
    // 问总共有多少条不同的路径？
    // 提示：
    // 1 <= m, n <= 100
    // 题目数据保证答案小于等于 2 * 109

    // 方法一：（自己写的）动态规划-时间复杂度：O(mn)，空间复杂度：O(n)
    public int uniquePaths(int m, int n) {
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        for (int i = 1; i < m; i++)
            for (int j = 1; j < n; j++)
                dp[j] = dp[j - 1] + dp[j];

        return dp[n - 1];
    }

    // 方法二：组合数学-时间复杂度：O(m)，空间复杂度：O(1)
    // C(m-1, m-1+n-1)
    public int uniquePaths2(int m, int n) {
        long ans = 1;
        for (int x = n, y = 1; y < m; ++x, ++y)
            ans = ans * x / y;

        return (int) ans;
    }

    // 66.加一
    // 给定一个由 整数 组成的 非空 数组所表示的非负整数，在该数的基础上加一。
    // 最高位数字存放在数组的首位， 数组中每个元素只存储单个数字。
    // 你可以假设除了整数 0 之外，这个整数不会以零开头。
    // 提示：
    // 1 <= digits.length <= 100
    // 0 <= digits[i] <= 9

    // 方法一：找出最长的后缀 9 -时间复杂度：O(n)，空间复杂度：O(1)
    public int[] plusOne(int[] digits) {
        int n = digits.length;
        for (int i = n - 1; i >= 0; --i) {
            if (digits[i] != 9) {
                ++digits[i];
                for (int j = i + 1; j < n; ++j)
                    digits[j] = 0;

                return digits;
            }
        }

        // digits 中所有的元素均为 9
        int[] ans = new int[n + 1];
        ans[0] = 1;
        return ans;
    }

    // 方法一：（自己写的）模拟-时间复杂度：O(n)，空间复杂度：O(1)
    public int[] plusOne11(int[] digits) {
        int n = digits.length;
        int adder = 1;
        for (int i = n - 1; i >= 0; i--) {
            int num = digits[i];
            digits[i] = (num + adder) % 10;
            adder = (num + adder) / 10;
            if (adder == 0)
                break;
        }
        if (adder == 0)
            return digits;

        int[] res = new int[n + 1];
        res[0] = 1;
        return res;
    }

    // 69. x的平方根
    // 给定一个非负整数 x ，计算并返回 x 的平方根，即实现 int sqrt(int x) 函数。
    // 正数的平方根有两个，只输出其中的正数平方根。
    // 如果平方根不是整数，输出只保留整数的部分，小数部分将被舍去。
    // 提示：
    // 0 <= x <= 231 - 1

    // 方法一：二分查找-时间复杂度：O(logx)。空间复杂度：O(1)。
    public int mySqrt(int x) {
        int left = 0;
        int right = x;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if ((long) mid * mid <= x) // 防止数据溢出，也可以直接把右边界设为(231 - 1)^(1/2)
                left = mid + 1;
            else
                right = mid - 1;

        }
        return right;
    }

    // 方法二：袖珍计算器算法-时间复杂度：O(1)。空间复杂度：O(1)。
    // 「袖珍计算器算法」是一种用指数函数 exp 和对数函数 ln 代替平方根函数的方法。
    // 我们通过有限的可以使用的数学函数，得到我们想要计算的结果。
    // 使用自然对数 e 进行换底
    public int mySqrt2(int x) {
        if (x == 0)
            return 0;

        int ans = (int) Math.exp(0.5 * Math.log(x));
        return (long) (ans + 1) * (ans + 1) <= x ? ans + 1 : ans;
        // return (int) Math.pow(x, 0.5);
    }

    // 方法三：牛顿迭代-时间复杂度：O(logx)。空间复杂度：O(1)。
    public int mySqrt3(int x) {
        if (x == 0)
            return 0;

        double C = x, x0 = x;
        while (true) {
            double xi = 0.5 * (x0 + C / x0);
            if (Math.abs(x0 - xi) < 1e-7)
                break;

            x0 = xi;
        }
        return (int) x0;
    }

    // 70.爬楼梯
    // 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
    // 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢？
    // 提示：
    // 1 <= n <= 45

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(1)
    // f(x) = f(x−1) + f(x−2)
    public int climbStairs(int n) {
        int p = 0, q = 0, r = 1;// 滚动数组，节省内存开销
        for (int i = 1; i <= n; ++i) {
            p = q;
            q = r;
            r = p + q;
        }
        return r;
    }

    // 方法二：矩阵快速幂-时间复杂度：同快速幂，O(logn)，空间复杂度：O(1)
    // f(n) = q^n * [f(1) f(0)]T
    // 快速计算矩阵 q 的 n 次幂，就可以得到 f(n) 的值。如果直接求取 q^n ，时间复杂度是 O(n) ，
    // 我们可以定义矩阵乘法，然后用快速幂算法来加速这里 q^n 的求取

    public int climbStairs2(int n) {
        int[][] q = { { 1, 1 }, { 1, 0 } };
        int[][] res = pow(q, n);
        return res[0][0];
    }

    // 快速幂
    public int[][] pow(int[][] a, int n) {
        int[][] ret = { { 1, 0 }, { 0, 1 } };
        while (n > 0) {
            if ((n & 1) == 1)
                ret = multiply(ret, a);

            n >>= 1;
            a = multiply(a, a);
        }
        return ret;
    }

    // 矩阵乘法
    public int[][] multiply(int[][] a, int[][] b) {
        int[][] c = new int[2][2];
        for (int i = 0; i < 2; i++)
            for (int j = 0; j < 2; j++)
                c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];

        return c;
    }

    // 方法三：通项公式-时空复杂度与pow函数和CPU 支持的指令集相关，这里不深入分析
    public int climbStairs3(int n) {
        double sqrt5 = Math.sqrt(5);
        double fibn = Math.pow((1 + sqrt5) / 2, n + 1) - Math.pow((1 - sqrt5) / 2, n + 1);
        return (int) Math.round(fibn / sqrt5);
    }

    // 73.矩阵置零
    // 给定一个 m x n 的矩阵，如果一个元素为 0 ，则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
    // 提示：
    // m == matrix.length
    // n == matrix[0].length
    // 1 <= m, n <= 200
    // -231 <= matrix[i][j] <= 231 - 1
    // 进阶：
    // 一个直观的解决方案是使用  O(mn) 的额外空间，但这并不是一个好的解决方案。
    // 一个简单的改进方案是使用 O(m + n) 的额外空间，但这仍然不是最好的解决方案。
    // 你能想出一个仅使用常量空间的解决方案吗？

    // 方法一：使用标记数组-时间复杂度：O(mn)，空间复杂度：O(m+n)

    // 方法二：使用两个标记变量-时间复杂度：O(mn)，空间复杂度：O(1)
    // 我们可以用矩阵的第一行和第一列代替方法一中的两个标记数组，以达到 O(1) 的额外空间。
    // 但这样会导致原数组的第一行和第一列被修改，无法记录它们是否原本包含 0。
    // 因此我们需要额外使用两个标记变量分别记录第一行和第一列是否原本包含 0。
    public void setZeroes2(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        boolean flagCol0 = false, flagRow0 = false;
        for (int i = 0; i < m; i++)
            if (matrix[i][0] == 0)
                flagCol0 = true;

        for (int j = 0; j < n; j++)
            if (matrix[0][j] == 0)
                flagRow0 = true;

        for (int i = 1; i < m; i++)
            for (int j = 1; j < n; j++)
                if (matrix[i][j] == 0)
                    matrix[i][0] = matrix[0][j] = 0;

        for (int i = 1; i < m; i++)
            for (int j = 1; j < n; j++)
                if (matrix[i][0] == 0 || matrix[0][j] == 0)
                    matrix[i][j] = 0;

        if (flagCol0)
            for (int i = 0; i < m; i++)
                matrix[i][0] = 0;

        if (flagRow0)
            for (int j = 0; j < n; j++)
                matrix[0][j] = 0;
    }

    // 方法二：（自己写的）使用两个标记变量-时间复杂度：O(mn)，空间复杂度：O(1)
    public void setZeroes22(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        boolean firstRowHasZero = false, firstColumnHasZero = false;
        for (int i = 0; i < m; i++)
            if (matrix[i][0] == 0) {
                firstColumnHasZero = true;
                break;
            }

        for (int j = 0; j < n; j++)
            if (matrix[0][j] == 0) {
                firstRowHasZero = true;
                break;
            }

        for (int i = 1; i < m; i++)
            for (int j = 1; j < n; j++)
                if (matrix[i][j] == 0) {
                    matrix[0][j] = 0;
                    matrix[i][0] = 0;
                }

        for (int i = 1; i < m; i++)
            if (matrix[i][0] == 0)
                for (int j = 1; j < n; j++)
                    matrix[i][j] = 0;

        for (int j = 1; j < n; j++)
            if (matrix[0][j] == 0)
                for (int i = 1; i < m; i++)
                    matrix[i][j] = 0;

        if (firstColumnHasZero)
            for (int i = 0; i < m; i++)
                matrix[i][0] = 0;

        if (firstRowHasZero)
            for (int j = 0; j < n; j++)
                matrix[0][j] = 0;
    }

    // 方法三：使用一个标记变量-时间复杂度：O(mn)，空间复杂度：O(1)
    // 我们可以对方法二进一步优化，只使用一个标记变量记录第一列是否原本存在 0。
    // 这样，第一列的第一个元素即可以标记第一行是否出现 0。
    // 但为了防止每一列的第一个元素被提前更新，我们需要从最后一行开始，倒序地处理矩阵元素。
    public void setZeroes3(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        boolean flagCol0 = false;
        for (int i = 0; i < m; i++) {
            if (matrix[i][0] == 0)
                flagCol0 = true;

            for (int j = 1; j < n; j++)
                if (matrix[i][j] == 0)
                    matrix[i][0] = matrix[0][j] = 0;
        }

        for (int i = m - 1; i >= 0; i--) {
            for (int j = 1; j < n; j++)
                if (matrix[i][0] == 0 || matrix[0][j] == 0)
                    matrix[i][j] = 0;

            if (flagCol0)
                matrix[i][0] = 0;
        }
    }

    // 75.颜色分类
    // 给定一个包含红色、白色和蓝色，一共 n 个元素的数组，原地对它们进行排序，使得相同颜色的元素相邻，并按照红色、白色、蓝色顺序排列。
    // 此题中，我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。
    // 示例 1：
    // 输入：nums = [2,0,2,1,1,0]
    // 输出：[0,0,1,1,2,2]
    // 示例 2：
    // 输入：nums = [2,0,1]
    // 输出：[0,1,2]
    // 进阶：
    // 你可以不使用代码库中的排序函数(nlogn)来解决这道题吗？
    // 你能想出一个仅使用常数空间的一趟扫描算法吗？

    // 方法一：单指针-时间复杂度：O(n)，空间复杂度：O(1)
    // 考虑对数组进行两次遍历。
    // 在第一次遍历中，我们将数组中所有的 0 交换到数组的头部
    // 在第二次遍历中，我们将数组中所有的 1 交换到头部的 0 之后（将数组中所有的 2 交换到数组的尾部。）
    public void sortColors(int[] nums) {
        int n = nums.length;
        int ptr = 0;
        // 在第一次遍历
        for (int i = 0; i < n; ++i) {
            if (nums[i] == 0) {
                int temp = nums[i];
                nums[i] = nums[ptr];
                nums[ptr] = temp;
                ++ptr;
            }
        }

        // 在第二次遍历
        for (int i = ptr; i < n; ++i) {
            if (nums[i] == 1) {
                int temp = nums[i];
                nums[i] = nums[ptr];
                nums[ptr] = temp;
                ++ptr;
            }
        }
    }

    // 方法二：双指针（三指针？指向0 1 跳2）-时间复杂度：O(n)，空间复杂度：O(1)
    // 方法一需要进行两次遍历，那么我们是否可以仅使用一次遍历呢？我们可以额外使用一个指针，即使用两个指针分别用来交换 0 和 1。
    // 从左向右遍历整个数组，遍历到 2 不作处理
    public void sortColors2(int[] nums) {
        int n = nums.length;
        // 用指针 p0 来交换 0，p1 来交换 1，初始索引都为 0。
        int p0 = 0, p1 = 0;
        for (int i = 0; i < n; ++i) {
            if (nums[i] == 1) {
                swap(nums, i, p1);
                // 只有p1右移
                ++p1;
            } else if (nums[i] == 0) {
                swap(nums, i, p0);
                // nums[i]==1，继续交换
                if (nums[i] == 1)
                    swap(nums, i, p1);

                // 都右移
                ++p0;
                ++p1;
            }
        }
    }

    // 方法三：双指针（三指针？指向0 2 跳1）-时间复杂度：O(n)，空间复杂度：O(1)
    // 考虑使用指针 p0 来交换 0，p2 来交换 2。此时，p0 的初始索引仍然为 0，而 p2 的初始索引为 n−1
    // 在遍历的过程中，我们需要找出所有的 0 交换至数组的头部，并且找出所有的 2 交换至数组的尾部。
    // 遍历到 1 不作处理
    public void sortColors3(int[] nums) {
        int n = nums.length;
        int p0 = 0, p2 = n - 1;
        // 如果遍历到的位置超过了 p2，那么就可以直接停止遍历了
        for (int i = 0; i <= p2; ++i) {
            // 当我们找到 2 时，我们需要不断地将其与 nums[p2] 进行交换，直到新的 nums[i] 不为 2。
            // 此时，如果 nums[i] 为 0，那么对应着第一种情况；如果 nums[i] 为 1，那么就不需要进行任何后续的操作。
            while (i <= p2 && nums[i] == 2) {
                swap(nums, i, p2);
                --p2;
            }

            // 注意处理 2 0 的先后顺序
            if (nums[i] == 0) {
                swap(nums, i, p0);
                ++p0;
            }
        }
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    // 76.最小覆盖子串
    // 给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串，则返回空字符串 "" 。
    // 注意：
    // 对于 t 中重复字符，我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
    // 如果 s 中存在这样的子串，我们保证它是唯一的答案。
    // 提示：
    // 1 <= s.length, t.length <= 105
    // s 和 t 由英文字母组成
    // 进阶：你能设计一个在 o(n) 时间内解决此问题的算法吗？

    // 方法一：滑动窗口-时间复杂度：O(C⋅∣s∣+∣t∣)，字符集大小为 C，空间复杂度：O(C)
    // 我们在 s 上滑动窗口，通过移动 r 指针不断扩张窗口。当窗口包含 t 全部所需的字符后，
    // 如果能收缩，我们就收缩窗口直到得到最小窗口。
    Map<Character, Integer> ori = new HashMap<Character, Integer>();// 字符串t的哈希表
    Map<Character, Integer> cnt = new HashMap<Character, Integer>();// （当前窗口内）字符计数的哈希表

    public String minWindow(String s, String t) {
        // 填充ori
        for (int i = 0; i < t.length(); i++) {
            char c = t.charAt(i);
            ori.put(c, ori.getOrDefault(c, 0) + 1);
        }

        int l = 0, r = -1;
        int len = Integer.MAX_VALUE, ansL = -1, ansR = -1;// 窗口大小，窗口左右边界
        int sLen = s.length();
        while (r < sLen) {
            ++r; // 一次移动一步
            if (r < sLen && ori.containsKey(s.charAt(r)))// 当前字符为目标t中的字符，加入计数哈希表
                cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1);

            while (check()) {// 当前窗口涵盖 t 所有字符的子串
                // 窗口大小变小，更新窗口边界
                if (r - l + 1 < len) {
                    len = r - l + 1;
                    ansL = l;
                    ansR = l + len;
                }
                // 准备移动窗口左边界，更新计数哈希表
                if (ori.containsKey(s.charAt(l)))
                    cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
                ++l;// 一次移动一步
            }
        }
        return ansL == -1 ? "" : s.substring(ansL, ansR);
    }

    // 检查当前窗口是否涵盖 t 所有字符的子串
    public boolean check() {
        Iterator<Map.Entry<Character, Integer>> iter = ori.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<Character, Integer> entry = iter.next();
            Character key = entry.getKey();
            Integer val = entry.getValue();
            if (cnt.getOrDefault(key, 0) < val)
                return false;

        }
        return true;
    }

    // 方法二：优化方法一，预处理 s，扔掉那些 t 中没有出现的字符，然后再做滑动窗口呢？
    // 或者在移动窗口时，一次跳多步

    // 方法二：自己写的（每次移动左，循环移动右，还可以每次移动右，循环移动左）
    public String minWindow2(String s, String t) {
        int n = s.length();
        Map<Character, Integer> strt = new HashMap<>();// 目标map
        Map<Character, Integer> window = new HashMap<>();// 窗口map
        for (char c : t.toCharArray())
            strt.put(c, strt.getOrDefault(c, 0) + 1);

        // 初始化窗口
        int right = -1;
        while (!check(strt, window)) {
            right++;
            if (right >= n)
                return "";
            char c = s.charAt(right);
            window.put(c, window.getOrDefault(c, 0) + 1);
        }
        int minLength = right + 1;
        int minLeft = 0;
        int minRight = right;

        // 每次移动左边界
        for (int left = 1; left < n; left++) {
            char removeChar = s.charAt(left - 1);
            if (strt.containsKey(removeChar)) {// 出窗元素在目标map里
                window.put(removeChar, window.get(removeChar) - 1);
                if (window.get(removeChar) < strt.get(removeChar)) {// 更新window后，不满足目标map
                    while (right < n) {// 移动右边界，直到遇到出窗元素
                        right++;
                        if (right >= n)
                            break;
                        char addChar = s.charAt(right);
                        window.put(addChar, window.getOrDefault(addChar, 0) + 1);
                        if (addChar == removeChar)
                            break;
                    }
                    if (right >= n)// 右边界越界
                        break;
                }
            }
            // 更新minLenth
            if (right - left + 1 < minLength) {
                minLength = right - left + 1;
                minLeft = left;
                minRight = right;
            }

        }

        return s.substring(minLeft, minRight + 1);
    }

    private boolean check(Map<Character, Integer> strt, Map<Character, Integer> window) {
        Set<Character> strtSet = strt.keySet();
        for (Character c : strtSet)
            if (window.getOrDefault(c, 0) < strt.get(c))
                return false;
        return true;
    }

    // 方法二：自己写的（每次移动右，循环移动左，调用比较函数更多，更推荐！）
    public String minWindow22(String s, String t) {
        Map<Character, Integer> sLetterCount = new HashMap<>();
        Map<Character, Integer> tLetterCount = new HashMap<>();
        int left = 0;
        int right = 0;
        int leftRes = 0;
        int rightRes = Integer.MAX_VALUE - 1;

        for (char c : t.toCharArray()) {
            tLetterCount.put(c, tLetterCount.getOrDefault(c, 0) + 1);
        }

        for (right = 0; right < s.length(); right++) {
            char cur = s.charAt(right);
            if (tLetterCount.containsKey(cur)) {
                sLetterCount.put(cur, sLetterCount.getOrDefault(cur, 0) + 1);
                if (contains(sLetterCount, tLetterCount)) {
                    while (true) {
                        char leftChar = s.charAt(left);
                        if (sLetterCount.containsKey(leftChar)) {
                            if (sLetterCount.get(leftChar) > tLetterCount.get(leftChar))
                                sLetterCount.put(leftChar, sLetterCount.get(leftChar) - 1);
                            else
                                break;
                        }
                        left++;
                    }

                    if (right - left + 1 < rightRes - leftRes + 1) {
                        leftRes = left;
                        rightRes = right;
                    }
                }
            }
        }
        if (rightRes == Integer.MAX_VALUE - 1)
            return "";
        else
            return s.substring(leftRes, rightRes + 1);
    }

    public boolean contains(Map<Character, Integer> sLetterCount, Map<Character, Integer> tLetterCount) {
        for (char c : tLetterCount.keySet()) {
            if (sLetterCount.getOrDefault(c, 0) < tLetterCount.get(c))
                return false;
        }
        return true;
    }

    // 方法三：（自己写的）作差优化，不使用比较函数
    public String minWindow3(String s, String t) {
        Map<Character, Integer> charCount = new HashMap<>();
        int sLen = s.length();
        int tLen = t.length();

        if (sLen < tLen)
            return "";

        for (int i = 0; i < tLen; i++) {
            char c = t.charAt(i);
            charCount.put(c, charCount.getOrDefault(c, 0) - 1);
        }

        int NegCount = charCount.size();

        int left = 0;
        int right = 0;
        int resLeft = 0;
        int resRight = Integer.MAX_VALUE;

        for (right = 0; right < sLen; right++) {
            char c = s.charAt(right);
            if (charCount.containsKey(c)) {
                charCount.put(c, charCount.get(c) + 1);
                if (charCount.get(c) == 0)
                    NegCount--;

                if (NegCount == 0) {
                    while (left < right) {
                        char remove = s.charAt(left);
                        if (charCount.containsKey(remove)) {
                            if (charCount.get(remove) == 0)
                                break;
                            charCount.put(remove, charCount.get(remove) - 1);
                        }
                        left++;
                    }
                    if (right - left < resRight - resLeft) {
                        resRight = right;
                        resLeft = left;
                    }
                }
            }
        }
        return resRight - resLeft == Integer.MAX_VALUE ? "" : s.substring(resLeft, resRight + 1);
    }

    // 78.子集
    // 给你一个整数数组 nums ，数组中的元素 互不相同 。返回该数组所有可能的子集（幂集）。
    // 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
    // 提示：
    // 1 <= nums.length <= 10
    // -10 <= nums[i] <= 10
    // nums 中的所有元素 互不相同
    // 方法一：迭代法实现子集枚举（位运算）-时间复杂度O(n×2^n)
    // 用 1 表示「在子集中」，0 表示「不在子集中」
    // 可以发现 0/10/1 序列对应的二进制数正好从 0 到 2^n - 1
    // 可以枚举mask∈[0,2^n−1]，mask 的二进制表示是一个 0/1 序列，可以按照这个 0/1 序列在原集合当中取数。
    // 当枚举完所有 2^n-1 个mask，也就能构造出所有的子集
    List<List<Integer>> subsets(int[] nums) {
        List<Integer> t = new ArrayList<Integer>();
        List<List<Integer>> ans = new ArrayList<List<Integer>>();

        int n = nums.length;
        for (int mask = 0; mask < (1 << n); ++mask) {
            t.clear();// 表t清空
            for (int i = 0; i < n; ++i)
                if ((mask & (1 << i)) != 0)
                    t.add(nums[i]);// 从末尾位开始，若为第i位为1则在t中加入对应元素（共n位）
            ans.add(new ArrayList<Integer>(t));
        }
        return ans;
    }

    // 方法二：递归法实现子集枚举（回溯、剪枝）-时间复杂度O(n×2^n)
    // 类似于树的深度优先遍历
    // 0左子树 1右子树
    class tsolution78 {
        List<Integer> t = new ArrayList<Integer>();
        List<List<Integer>> ans = new ArrayList<List<Integer>>();

        List<List<Integer>> subsets(int[] nums) {
            dfs(0, nums);
            return ans;
        }

        void dfs(int cur, int[] nums) {
            if (cur == nums.length) {// 此时所有位已枚举完成，（回溯、剪枝）
                ans.add(new ArrayList<Integer>(t));
                return;
            }
            // 选择加入当前位置cur的元素
            t.add(nums[cur]);// 置于末尾
            dfs(cur + 1, nums);

            // 选择不加入当前位置cur的元素
            t.remove(t.size() - 1);// 移除末尾元素
            dfs(cur + 1, nums);
        }
    }

    // 79.单词搜索
    // 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中，返回 true ；否则，返回 false 。
    // 单词必须按照字母顺序，通过相邻的单元格内的字母构成，其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。
    // 同一个单元格内的字母不允许被重复使用。
    // 提示：
    // m == board.length
    // n = board[i].length
    // 1 <= m, n <= 6
    // 1 <= word.length <= 15
    // board 和 word 仅由大小写英文字母组成
    // 进阶：你可以使用搜索剪枝的技术来优化解决方案，使其在 board 更大的情况下可以更快解决问题？

    // 方法一：回溯-时间复杂度：O(MN 3^L)（实际远小于此），空间复杂度：O(MN)
    char[][] myBoard;
    String myWord;
    // 标记是否被访问过
    boolean[][] visited;

    public boolean exist(char[][] board, String word) {
        int h = board.length, w = board[0].length;
        myBoard = board;
        myWord = word;
        visited = new boolean[h][w];
        for (int i = 0; i < h; i++)
            for (int j = 0; j < w; j++)
                if (check(i, j, 0))
                    return true;

        return false;
    }

    // 当前坐标i j，当前递归到的字符位置k
    public boolean check(int i, int j, int k) {
        if (myBoard[i][j] != myWord.charAt(k))
            return false;
        else if (k == myWord.length() - 1)
            return true;

        visited[i][j] = true;
        // 上下左右四个方向（实际最多三个有可能）
        int[][] directions = { { 0, 1 }, { 0, -1 }, { 1, 0 }, { -1, 0 } };

        for (int[] dir : directions) {
            // 下一步
            int newi = i + dir[0], newj = j + dir[1];
            // 判断是否越界
            if (newi >= 0 && newi < myBoard.length && newj >= 0 && newj < myBoard[0].length)
                // 判断是否访问过
                if (!visited[newi][newj])
                    if (check(newi, newj, k + 1))
                        return true;

        }
        // 结束递归时还原
        visited[i][j] = false;
        return false;
    }

    // 84.柱状图中最大的矩形
    // 给定 n 个非负整数，表示柱状图中各个柱子的高度。每个柱子彼此相邻，且宽度为 1
    // 求在该柱状图中，能够勾勒出来的矩形的最大面积
    // 提示：
    // 1 <= heights.length <=105
    // 0 <= heights[i] <= 104

    // 方法一：单调栈 两次遍历-时空复杂度：O(n)
    // 从左至右（单调递增）、从右至左（单调递增）两次遍历
    public int largestRectangleArea(int[] heights) {
        int n = heights.length;
        int[] left = new int[n];// 包含当前柱子的最大矩形的左开区间
        int[] right = new int[n];// 包含当前柱子的最大矩形的右开区间

        Deque<Integer> mono_stack = new LinkedList<>();// 单调栈 单调递增，存放下标
        for (int i = 0; i < n; ++i) {
            // 栈非空，且当前元素小于栈顶元素（不满足单调递增），循环出栈直到满足单调递增

            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i])
                mono_stack.pop();

            if (mono_stack.isEmpty())
                left[i] = -1; // 1.第一个元素，2.当前为最小元素，左边全比他高
            else
                left[i] = mono_stack.peek(); // 左边比当前高，即为左开区间
            mono_stack.push(i);
        }

        mono_stack.clear();// 清空栈
        for (int i = n - 1; i >= 0; --i) {
            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i])
                mono_stack.pop();

            // 1.第一个被遍历的元素，2.当前为最小元素，右边全比他高
            right[i] = (mono_stack.isEmpty() ? n : mono_stack.peek());
            mono_stack.push(i);
        }

        int ans = 0;
        for (int i = 0; i < n; ++i) // 宽度 = 左右开区间差值 - 1
            ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);

        return ans;
    }

    // 方法二：单调栈 + 常数优化 只遍历一次-时空复杂度：O(n)
    // 位置 i 元素入栈时，确定了它的左边界。出栈时确定了右边界
    public int largestRectangleArea2(int[] heights) {
        int n = heights.length;
        int[] left = new int[n];
        int[] right = new int[n];
        Arrays.fill(right, n);// 初始为n 1.最后一个元素 2.右边全比他高（遍历完都没有出栈）

        Deque<Integer> mono_stack = new LinkedList<>();// 单调栈 单调递增，存放下标
        for (int i = 0; i < n; ++i) {
            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
                right[mono_stack.peek()] = i;// 出栈时确定了右边界
                mono_stack.pop();
            }
            left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek());
            mono_stack.push(i);// 入栈时，确定了它的左边界
        }

        int ans = 0;
        for (int i = 0; i < n; ++i) // 宽度 = 左右开区间差值 - 1
            ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);

        return ans;
    }

    // 方法二：（自己写的）单调栈 + 只遍历一次 + 引入边界方便计算 + 出栈入栈计算同步进行
    class ssoltion_84 {
        Deque<int[]> stkIndexHeight = new LinkedList<>();// 单调栈，单调递增
        int res = 0;
        int n;

        public int largestRectangleArea22(int[] heights) {
            n = heights.length;
            stkIndexHeight.push(new int[] { -1, -1 });// 左边界，一定不会出栈，则后续不用判断栈空
            for (int i = 0; i < n; i++) {
                int cur = heights[i];
                popAndCalculate(cur, i);
                stkIndexHeight.push(new int[] { i, cur });
            }
            popAndCalculate(0, n);// 右边界，使得栈中剩余元素出栈，出栈直至左边界为止，得到计算值
            return res;
        }

        private void popAndCalculate(int curHeight, int index) {
            while (curHeight < stkIndexHeight.peek()[1]) {
                int height = stkIndexHeight.pop()[1];
                int leftIndex = stkIndexHeight.peek()[0];
                int area = height * (index - leftIndex - 1);
                res = Math.max(res, area);
            }
        }
    }

    // 88.合并两个有序数组
    // 给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2，另有两个整数 m 和 n ，分别表示 nums1 和 nums2 中的元素数目。
    // 请你 合并 nums2 到 nums1 中，使合并后的数组同样按 非递减顺序 排列。
    // 注意：最终，合并后数组不应由函数返回，而是存储在数组 nums1 中。
    // 为了应对这种情况，nums1 的初始长度为 m + n，其中前 m 个元素表示应合并的元素，后 n 个元素为 0 ，应忽略。nums2 的长度为 n 。
    // 提示：
    // nums1.length == m + n
    // nums2.length == n
    // 0 <= m, n <= 200
    // 1 <= m + n <= 200
    // -109 <= nums1[i], nums2[j] <= 109
    // 进阶：你可以设计实现一个时间复杂度为 O(m + n) 的算法解决此问题吗？

    // 方法一：逆向双指针-时间复杂度：O(m+n)，空间复杂度：O(1)
    // 指针设置为从后向前遍历，每次取两者之中的较大者放进 A 的最后面。
    public void merge(int[] A, int m, int[] B, int n) {
        int pa = m - 1, pb = n - 1;
        int tail = m + n - 1;
        int cur;
        while (pa >= 0 || pb >= 0) {
            if (pa == -1)
                cur = B[pb--];
            else if (pb == -1)
                cur = A[pa--];
            else if (A[pa] > B[pb])
                cur = A[pa--];
            else
                cur = B[pb--];
            A[tail--] = cur;
        }
    }

    // 方法二：双指针-时间复杂度：O(m+n)，空间复杂度：O(m+n)
    // 合并到新数组，再复制到A数组
    public void merge2(int[] A, int m, int[] B, int n) {
        int[] C = new int[n + m];
        int aindex = 0, bindex = 0, cindex = 0;
        while (aindex < m && bindex < n) {
            if (A[aindex] < B[bindex])
                C[cindex++] = A[aindex++];
            else
                C[cindex++] = B[bindex++];
        }
        while (aindex != m)
            C[cindex++] = A[aindex++];
        while (bindex != n)
            C[cindex++] = B[bindex++];

        for (int i = 0; i < m + n; i++)
            A[i] = C[i];
    }

    // 方法三：直接合并后排序-时间复杂度：O((m+n)log(m+n))，空间复杂度：O(log(m+n))

    // 91.解码方法
    // 一条包含字母 A-Z 的消息通过以下映射进行了 编码 ：
    // 'A' -> "1"
    // 'B' -> "2"
    // ...
    // 'Z' -> "26"
    // 要 解码 已编码的消息，所有数字必须基于上述映射的方法，反向映射回字母（可能有多种方法）。例如，"11106" 可以映射为：
    // "AAJF" ，将消息分组为 (1 1 10 6)
    // "KJF" ，将消息分组为 (11 10 6)
    // 注意，消息不能分组为  (1 11 06) ，因为 "06" 不能映射为 "F" ，这是由于 "6" 和 "06" 在映射中并不等价。
    // 给你一个只含数字的 非空 字符串 s ，请计算并返回 解码 方法的 总数 。
    // 题目数据保证答案肯定是一个 32 位 的整数。
    // 提示：
    // 1 <= s.length <= 100
    // s 只包含数字，并且可能包含前导零。

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(n)
    public int numDecodings(String s) {
        int n = s.length();
        int[] f = new int[n + 1];
        f[0] = 1;
        for (int i = 1; i <= n; ++i) {
            if (s.charAt(i - 1) != '0')
                f[i] += f[i - 1];

            if (i > 1 && s.charAt(i - 2) != '0' && ((s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0') <= 26))
                f[i] += f[i - 2];
        }
        return f[n];
    }

    // 方法一：动态规划（空间优化）-时间复杂度：O(n)，空间复杂度：O(1)
    public int numDecodings11(String s) {
        int n = s.length();
        // a = f[i-2], b = f[i-1], c=f[i]
        int a = 0, b = 1, c = 0;
        for (int i = 1; i <= n; ++i) {
            c = 0;
            if (s.charAt(i - 1) != '0')
                c += b;

            if (i > 1 && s.charAt(i - 2) != '0' && ((s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0') <= 26))
                c += a;

            a = b;
            b = c;
        }
        return c;
    }

    // 方法二：dfs 会超时

    // 94.二叉树的中序遍历
    // 给定一个二叉树的根节点 root ，返回它的 中序 遍历

    // 方法一：递归DFS-空间复杂度O(n)
    // 递归特性：1.节点为空，2.节点不为空
    List<Integer> inorderTraversal(TreeNode root) {

        List<Integer> res = new ArrayList<Integer>();
        inorder(root, res);
        return res;
    }

    void inorder(TreeNode root, List<Integer> res) {
        if (root == null)// 根为空，直接返回
            return;
        inorder(root.left, res);// 左
        res.add(root.val);// 根
        inorder(root.right, res);// 右
    }

    // 方法二：栈（迭代）-空间复杂度O(n)
    // 左根右-对于一个节点：入栈（循环遍历左孩子）、出栈（记录val）、（指针指向出栈节点右孩子）
    List<Integer> inorderTraversal2(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Deque<TreeNode> stk = new LinkedList<>();// 速度比Stack快
        while (root != null || !stk.isEmpty()) {// 节点为空指针且栈为空时才跳出
            while (root != null) {// 节点非空
                stk.push(root);// 节点入栈
                root = root.left;// 循环遍历左孩子
            }
            root = stk.pop();// root指向栈顶节点，栈顶节点出栈
            res.add(root.val);// 出栈时记录val
            root = root.right;// 遍历出栈节点右孩子
        }
        return res;
    }

    // 方法三：Morris中序遍历-线索二叉树（找前驱）-空间复杂度O(1)
    // 步骤如下（假设当前遍历到的节点为 x）：
    // 1.如果 x 无左孩子，先将 x 的值加入答案数组，再访问 x 的右孩子。
    // 2.如果 x 有左孩子，则找到 x 左子树上最右的节点（即左子树中序遍历的最后一个节点，x 在中序遍历中的前驱节点）
    // 记为 predecessor。根据 predecessor 的右孩子是否为空，进行如下操作
    // 1.如果 predecessor 的右孩子为空，则将其右孩子指向 x，然后访问 x 的左孩子
    // 2.如果 predecessor 的右孩子不为空，则此时其右孩子指向 x，说明已经遍历完 x 的左子树，
    // 将 predecessor 的右孩子置空，将 x 的值加入答案数组，然后访问 x 的右孩子
    // 3.重复上述操作，直至访问完整棵树
    List<Integer> inorderTraversal3(TreeNode root) {
        List<Integer> res = new ArrayList<Integer>();
        TreeNode predecessor = null;

        while (root != null) {
            if (root.left != null) {
                // predecessor 节点就是当前 root 左子树最右，其右指针必为空
                predecessor = root.left;
                while (predecessor.right != null && predecessor.right != root) {
                    predecessor = predecessor.right;
                }

                // predecessor 的右指针为空，指向 root，继续遍历左子树
                if (predecessor.right == null) {
                    predecessor.right = root;
                    root = root.left;
                }
                // 右指针非空，指向root，即root之前的节点都已遍历
                else {
                    res.add(root.val);
                    predecessor.right = null;
                    root = root.right;
                }
            }
            // 如果没有左孩子，则直接访问右孩子
            else {
                res.add(root.val);
                root = root.right;
            }
        }
        return res;
    }

    // 98.验证二叉搜索树
    // 给定一个二叉树，判断其是否是一个有效的二叉搜索树
    // 假设一个二叉搜索树具有如下特征：
    // 节点的左子树只包含小于当前节点的数
    // 节点的右子树只包含大于当前节点的数
    // 所有左子树和右子树自身必须也是二叉搜索树
    // 提示：
    // 树中节点数目范围在[1, 104] 内
    // -231 <= Node.val <= 231 - 1 （注意溢出！）

    // 方法一：递归 + 传值（上下界）判断-时空复杂度O(n)
    // 递归特性：①为空，返回；②节点值是否处于上下界中？
    // 维护一个「上下界」
    // 「并不是单纯只满足」：左子树值小于根节点值，右子树值大于根节点值
    boolean isValidBST(TreeNode root) {
        return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    boolean isValidBST(TreeNode node, long lower, long upper) {
        if (node == null)
            return true;

        if (node.val <= lower || node.val >= upper)
            return false;

        return isValidBST(node.left, lower, node.val) && isValidBST(node.right, node.val, upper);
        // 可提前return（剪枝）
    }

    // 方法二：中序遍历（迭代）+ 比较值-时空复杂度O(n)
    boolean isValidBST2(TreeNode root) {
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        double inorder = -Double.MAX_VALUE;

        while (!stack.isEmpty() || root != null) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            // 如果中序遍历得到的节点的值小于等于前一个 inorder，说明不是二叉搜索树
            if (root.val <= inorder)
                return false;
            inorder = root.val;
            root = root.right;
        }
        return true;
    }

    // 方法三：中序遍历（递归） + 比较值

    // 101.对称二叉树
    // 给定一个二叉树，检查它是否是镜像对称的
    // （都重复计算了一遍）

    // 方法一：递归DFS-时间复杂度O(n)
    boolean isSymmetric(TreeNode root) {
        return check(root, root);
    }

    boolean check(TreeNode p, TreeNode q) {
        if (p == null && q == null)
            return true;
        else if (p == null || q == null)
            return false;

        return p.val == q.val && check(p.left, q.right) && check(p.right, q.left);
        // 可通过p.val != q.val 提前跳出
    }

    // 方法二：迭代BFS（队列，null也要入队）-时间复杂度O(n)
    boolean isSymmetric2(TreeNode root) {
        TreeNode u = root, v = root;
        Queue<TreeNode> q = new LinkedList<TreeNode>();
        // 根节点入队
        q.offer(u);
        q.offer(v);
        while (!q.isEmpty()) {
            // 成对出队，前后顺序不能错
            u = q.poll();
            v = q.poll();

            if (u == null && v == null)
                continue;// 进入下一层循环
            if ((u == null || v == null) || (u.val != v.val))
                return false;
            // 成对入队，前后顺序不能错
            // 即使左右子树为null（无需判断）也需要入队，以判断是否对称
            q.offer(u.left);
            q.offer(v.right);

            q.offer(u.right);
            q.offer(v.left);
        }
        return true;
    }

    // 方法三：自己写的迭代DFS（栈）（要改造为null也要入栈，不建议）-时间复杂度O(n)
    boolean isSymmetric3(TreeNode root) {
        TreeNode u = root, v = root;
        Deque<TreeNode> s = new LinkedList<TreeNode>();
        do {
            while (u != null && v != null) {
                // 成对入队
                s.push(u);
                s.push(v);
                u = u.left;
                v = v.right;
            }
            // 不同时为null
            if ((u == null) != (v == null))
                return false;
            // 成对出队，前后顺序不能错
            v = s.pop();
            u = s.pop();
            // u v不一样，或者值不相同
            if ((u == null) != (v == null))
                return false;
            else if (u.val != v.val)
                return false;
            u = u.right;
            v = v.left;
        } while (!s.isEmpty());// 栈为空时，u v第二次到达root节点，已对比完毕
        return true;
    }
    // 102.二叉树的层序遍历
    // 给你一个二叉树，请你返回其按「层序遍历」得到的节点值。 （即逐层地，从左到右访问所有节点）

    // 方法一：BFS-多元素拓展-O(n)
    // 和普通广度优先搜索的区别在于：
    // 普通广度优先搜索每次只取一个元素拓展，而这里每次取i个元素。在上述过程中的第 i 次迭代就得到了二叉树的第 i 层的 i个元素
    List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<List<Integer>>();
        if (root == null)
            return ret;

        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);// 根节点入队
        while (!queue.isEmpty()) {
            List<Integer> level = new ArrayList<Integer>();// 每一层
            int currentLevelSize = queue.size();// 获取当前层大小
            for (int i = 1; i <= currentLevelSize; ++i) {
                TreeNode node = queue.poll();
                level.add(node.val);
                if (node.left != null)
                    queue.offer(node.left);
                if (node.right != null)
                    queue.offer(node.right);
            }
            ret.add(level);
        }

        return ret;
    }

    // 方法二：BFS-自己写的lowb双队列-O(n)
    List<List<Integer>> levelOrder2(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<List<Integer>>();
        if (root == null)
            return ret;

        Queue<TreeNode> q1 = new LinkedList<TreeNode>();
        Queue<TreeNode> q2 = new LinkedList<TreeNode>();
        List<Integer> level = new ArrayList<Integer>();// 每一层
        q1.offer(root);// 根节点入队
        Queue<TreeNode> qout = q1, qin = q2;

        while (!qin.isEmpty() || !qout.isEmpty()) {
            TreeNode node = qout.poll();// 出队
            level.add(node.val);
            if (node.left != null)
                qin.offer(node.left);
            if (node.right != null)
                qin.offer(node.right);

            if (qout.isEmpty()) {// 最后一层遍历结束时，两个队列都为空，写入ret，再while判断退出
                // 交换qin qout
                qout = (qout == q1) ? q2 : q1;
                qin = (qin == q1) ? q2 : q1;
                ret.add(level);
                level = new ArrayList<Integer>();// 不能用clear，要new一个List
            }
        }
        return ret;
    }

    // 103.二叉树的锯齿形层序遍历
    // 给你二叉树的根节点 root ，返回其节点值的 锯齿形层序遍历 。（即先从左往右，再从右往左进行下一层遍历，以此类推，层与层之间交替进行）。
    // 提示：
    // 树中节点数目在范围 [0, 2000] 内
    // -100 <= Node.val <= 100

    // 方法一：bfs-时间复杂度：O(n)，空间复杂度：O(n)
    public List<List<Integer>> zigzagLevelOrder(TreeNode root) {
        List<List<Integer>> ans = new LinkedList<List<Integer>>();
        if (root == null)
            return ans;

        Queue<TreeNode> nodeQueue = new ArrayDeque<TreeNode>();
        nodeQueue.offer(root);
        boolean isOrderLeft = true;

        while (!nodeQueue.isEmpty()) {
            Deque<Integer> levelList = new LinkedList<Integer>();
            int size = nodeQueue.size();
            for (int i = 0; i < size; ++i) {
                TreeNode curNode = nodeQueue.poll();
                if (isOrderLeft)
                    levelList.offerLast(curNode.val);
                else
                    levelList.offerFirst(curNode.val);

                if (curNode.left != null)
                    nodeQueue.offer(curNode.left);
                if (curNode.right != null)
                    nodeQueue.offer(curNode.right);
            }
            ans.add(new LinkedList<Integer>(levelList));
            isOrderLeft = !isOrderLeft;
        }
        return ans;
    }

    // 104.二叉树的最大深度
    // 给定一个二叉树，找出其最大深度。
    // 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
    // 说明: 叶子节点是指没有子节点的节点。

    // 方法一：DFS（后序遍历）-空间复杂度-O(h)
    int maxDepth(TreeNode root) {// 三元运算符比if-else更快
        return root == null ? 0 : Math.max(maxDepth(root.left), maxDepth(root.right)) + 1;// 省去创建局部变量
    }

    // 方法一：（自己写的）dfs（先序遍历）-空间复杂度-O(h)
    class tsolution104 {
        int res = 0;

        public int maxDepth(TreeNode root) {
            dfs(root, 1);
            return res;
        }

        private void dfs(TreeNode root, int curDepth) {
            if (root == null)
                return;
            res = Math.max(res, curDepth);
            dfs(root.left, curDepth + 1);
            dfs(root.right, curDepth + 1);
        }
    }

    // 方法二：BFS-空间复杂度-O(n)
    int maxDepth2(TreeNode root) {
        if (root == null)
            return 0;
        Queue<TreeNode> queue = new LinkedList<TreeNode>();
        queue.offer(root);
        int ans = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode node = queue.poll();
                if (node.left != null)
                    queue.offer(node.left);
                if (node.right != null)
                    queue.offer(node.right);
            }
            ans++;
        }
        return ans;
    }

    // 105.从前序与中序遍历序列构造二叉树
    // 给定一棵树的前序遍历 preorder 与中序遍历 inorder。请构造二叉树并返回其根节点。
    // 1 <= preorder.length <= 3000
    // inorder.length == preorder.length
    // -3000 <= preorder[i], inorder[i] <= 3000
    // preorder 和 inorder 「均无重复元素」
    // inorder 均出现在 preorder
    // preorder 保证为二叉树的前序遍历序列
    // inorder 保证为二叉树的中序遍历序列

    // 方法一：递归-时间复杂度、空间复杂度：O(n)
    // 无重复元素，则可用哈希表
    // 递归时，需要传入前序、中序的左右边界
    Map<Integer, Integer> indexMap;

    TreeNode buildTree(int[] preorder, int[] inorder) {
        int n = preorder.length;
        // 构造哈希映射，值——→索引，以便快速定位中序遍历中对应根节点的索引值，而前序遍历的根节点一定是区间内第一个节点
        indexMap = new HashMap<Integer, Integer>();
        for (int i = 0; i < n; i++)
            indexMap.put(inorder[i], i);
        return myBuildTree(preorder, inorder, 0, n - 1, 0, n - 1);
    }

    // ========================前序遍历========中序遍历=====前序遍历索引的左区间，右区间============中序遍历索引的左区间，右区间
    TreeNode myBuildTree(int[] preorder, int[] inorder, int preorder_left, int preorder_right, int inorder_left,
            int inorder_right) {
        // 前序遍历的形式:[ 根节点, [左子树的前序遍历结果], [右子树的前序遍历结果] ]，即根节点总是前序遍历中的第一个节点
        // 中序遍历的形式:[ [左子树的中序遍历结果], 根节点, [右子树的中序遍历结果] ]
        // 前序遍历的左区间用来确定根节点，右区间用来确定是否有子树，没有则提前返回null
        // 中序遍历的根节点由前序遍历的左区间确定，左区间确定左子树的节点数目
        if (preorder_left > preorder_right)// 单独使用前序遍历，或中序遍历的区间范围来判断都可行
            return null;

        int preorder_root = preorder_left;// 前序遍历中的第一个节点就是根节点
        int inorder_root = indexMap.get(preorder[preorder_root]);// 在中序遍历中定位根节点
        TreeNode root = new TreeNode(preorder[preorder_root]); // 先把根节点建立出来
        int size_left_subtree = inorder_root - inorder_left;// 得到左子树中的节点数目
        // 递归地构造左右子树，并连接到根节点
        // 先序遍历左子树边界[左边界+1, 左边界+size_left_subtree]，中序遍历左子树边界[左边界, 根节点定位-1]
        root.left = myBuildTree(preorder, inorder, preorder_left + 1, preorder_left + size_left_subtree, inorder_left,
                inorder_root - 1);
        // 先序遍历右子树边界「左边界+1+左子树节点数目, 右边界」，中序遍历右子树边界「根节点定位+1, 右边界」
        root.right = myBuildTree(preorder, inorder, preorder_left + size_left_subtree + 1, preorder_right,
                inorder_root + 1, inorder_right);
        return root;
    }

    // 方法二：迭代-时间复杂度、空间复杂度：O(n)
    // 对于前序遍历中的任意两个连续节点 u 和 v，根据前序遍历的流程，可知 u 和 v 只有两种可能的关系：
    // 1. v 是 u 的左孩子。这是因为在遍历到 u 之后，下一个遍历的节点就是 u 的左孩子，即 v
    // 2. u 没有左孩子，并且 v 是 u 的某个祖先节点（或者 u 本身）的右孩子。如果 u 没有左孩子，那么下一个遍历的节点就是 u 的右孩子
    // 如果 u 没有右孩子，我们就会向上回溯，直到遇到第一个有右孩子（且 u 不在它的右孩子的子树中）的节点 u_a，那么 v 就是 u_a 的右孩子
    TreeNode buildTree2(int[] preorder, int[] inorder) {
        if (preorder == null) // 空树提前返回null
            return null;
        TreeNode root = new TreeNode(preorder[0]);
        // 用一个栈 stack 来维护「当前节点的所有还没有考虑过右孩子的祖先节点」，栈顶就是当前节点。也就是说，只有在栈中的节点才可能连接一个新的右孩子
        // 有点类似于中序遍历的迭代中的栈
        Deque<TreeNode> stack = new LinkedList<TreeNode>();
        stack.push(root);// 根节点入栈
        int inorderIndex = 0;// 指向中序遍历的某个位置，当前节点不断往左走达到的最终节点
        for (int i = 1; i < preorder.length; i++) {
            int preorderVal = preorder[i];// 例子中的节点v，根节点是节点u
            TreeNode node = stack.peek();// 根节点出栈
            if (node.val != inorder[inorderIndex]) {// 情况1. 节点v是u的左孩子
                // 若inorderIndex已是不断往左走达到的最终节点（节点u没有左孩子），则进入情况2.
                node.left = new TreeNode(preorderVal);
                stack.push(node.left);// 没有考虑过右孩子的节点通通入栈
            } else {// 情况2. u 没有左孩子，向上回溯
                while (!stack.isEmpty() && stack.peek().val == inorder[inorderIndex]) {
                    node = stack.pop();
                    inorderIndex++;
                }
                node.right = new TreeNode(preorderVal);
                stack.push(node.right);// 没有考虑过右孩子的节点通通入栈
            }
        }
        return root;
    }

    // 108.将有序数组转换为二叉搜索树
    // 给你一个整数数组 nums ，其中元素已经按 升序 排列，请你将其转换为一棵 高度平衡 二叉搜索树。
    // 高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。
    // 提示：
    // 1 <= nums.length <= 104
    // -104 <= nums[i] <= 104
    // nums 按 严格递增 顺序排列

    // 方法一：dfs-时间复杂度：O(n)，空间复杂度：O(logn)
    public TreeNode sortedArrayToBST(int[] nums) {
        return helper(nums, 0, nums.length - 1);
    }

    public TreeNode helper(int[] nums, int left, int right) {
        if (left > right)
            return null;

        // 总是选择中间位置左边的数字作为根节点
        int mid = (left + right) / 2;

        TreeNode root = new TreeNode(nums[mid]);
        root.left = helper(nums, left, mid - 1);
        root.right = helper(nums, mid + 1, right);
        return root;
    }

    // 116.填充每个节点的下一个右侧节点指针
    // 给定一个 完美二叉树 ，其所有叶子节点都在同一层，每个父节点都有两个子节点。
    // 填充它的每个 next 指针，让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点，则将 next 指针设置为 NULL。
    // 初始状态下，所有 next 指针都被设置为 NULL。
    // struct Node {
    // int val;
    // Node *left;
    // Node *right;
    // Node *next;
    // }
    // 提示：
    // 树中节点的数量在 [0, 212 - 1] 范围内
    // -1000 <= node.val <= 1000
    // 进阶：
    // 你只能使用常量级额外空间。
    // 使用递归解题也符合要求，本题中递归程序占用的栈空间不算做额外的空间复杂度。

    // 方法一：层次遍历-时间复杂度：O(n)，空间复杂度：O(n)
    public Node connect(Node root) {
        if (root == null)
            return root;

        Queue<Node> queue = new LinkedList<Node>();// 初始化队列同时将第一层节点加入队列中，即根节点
        queue.add(root);

        // 外层的 while 循环迭代的是层数
        while (!queue.isEmpty()) {

            int size = queue.size();// 记录当前队列大小
            // 遍历这一层的所有节点
            for (int i = 0; i < size; i++) {
                Node node = queue.poll();// 从队首取出元素
                if (i < size - 1)// 连接
                    node.next = queue.peek();

                // 拓展下一层节点
                if (node.left != null)
                    queue.add(node.left);
                if (node.right != null)
                    queue.add(node.right);
            }
        }
        return root;// 返回根节点
    }

    // 方法二：使用已建立的 next 指针-时间复杂度：O(n)，空间复杂度：O(1)
    // 处理好下一层的连接关系后，再前往下一层
    public Node connect2(Node root) {
        if (root == null)
            return root;

        Node leftmost = root;// 从根节点开始
        while (leftmost.left != null) {
            Node head = leftmost;// 遍历这一层节点组织成的链表，为下一层的节点更新 next 指针
            while (head != null) {
                // CONNECTION 1
                head.left.next = head.right;
                // CONNECTION 2
                if (head.next != null)// 不是该层最后一个节点
                    head.right.next = head.next.left;

                head = head.next;// 指针向后移动
            }
            leftmost = leftmost.left;// 去下一层的最左的节点
        }
        return root;
    }

    // 方法三：（自己写的）dfs-时间复杂度：O(n)，空间复杂度：O(1) 按照题意：本题中递归程序占用的栈空间不算做额外的空间复杂度。
    // 题意中是完美二叉树（完全二叉树），才可以这么做
    class ssoltion_116 {
        public Node connect3(Node root) {
            if (root == null)
                return root;
            dfs(root.left, root.right);
            return root;
        }

        private void dfs(Node leftRoot, Node rightRoot) {
            if (leftRoot == null || rightRoot == null)
                return;
            leftRoot.next = rightRoot;
            Node leftLeft = leftRoot.left, leftRight = leftRoot.right;
            Node rightLeft = rightRoot.left, rightRight = rightRoot.right;
            dfs(leftLeft, leftRight);
            dfs(leftRight, rightLeft);
            dfs(rightLeft, rightRight);
        }
    }

    // 118.杨辉三角
    // 给定一个非负整数 numRows，生成「杨辉三角」的前 numRows 行。
    // 在「杨辉三角」中，每个数是它左上方和右上方的数的和。
    // 提示:
    // 1 <= numRows <= 30

    // 方法一：数学-时间复杂度：O(n^2)，空间复杂度：O(1)
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> ret = new ArrayList<List<Integer>>();
        for (int i = 0; i < numRows; ++i) {
            List<Integer> row = new ArrayList<Integer>();
            for (int j = 0; j <= i; ++j) {
                if (j == 0 || j == i)
                    row.add(1);
                else
                    row.add(ret.get(i - 1).get(j - 1) + ret.get(i - 1).get(j));
            }
            ret.add(row);
        }
        return ret;
    }

    // 121.买卖股票的最佳时机
    // 给定一个数组 prices ，它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
    // 你只能选择 某一天 买入这只股票，并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
    // 返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 0 。
    // 提示：
    // 1 <= prices.length <= 105
    // 0 <= prices[i] <= 104

    // 方法一：暴力法-时间复杂度：O(n^2)，空间复杂度：O(1)

    // 方法二：一次遍历（应该不算动规，算贪心吧）-时间复杂度：O(n)，空间复杂度：O(1)
    public int maxProfit(int prices[]) {
        int minprice = Integer.MAX_VALUE;
        int maxprofit = 0;
        for (int i = 0; i < prices.length; i++) {
            if (prices[i] < minprice)// 更新最小价
                minprice = prices[i];
            else if (prices[i] - minprice > maxprofit)// 更新最大收益
                maxprofit = prices[i] - minprice;

        }
        return maxprofit;
    }

    // 122.买卖股票的最佳时机II
    // 给你一个整数数组 prices ，其中 prices[i] 表示某支股票第 i 天的价格。
    // 在每一天，你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买，然后在 同一天 出售。
    // 返回 你能获得的 最大 利润 。
    // 提示：
    // 1 <= prices.length <= 3 * 104
    // 0 <= prices[i] <= 104

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(1)
    // 「不能同时参与多笔交易」，因此每天交易结束后只可能存在手里有一支股票或者没有股票的状态
    class tsolution122 {
        public int maxProfit(int[] prices) {
            int n = prices.length;
            int dp0 = 0, dp1 = -prices[0];
            for (int i = 1; i < n; ++i) {
                int newDp0 = Math.max(dp0, dp1 + prices[i]);
                int newDp1 = Math.max(dp1, dp0 - prices[i]);
                dp0 = newDp0;
                dp1 = newDp1;
            }
            return dp0;
        }
    }

    // 方法二：贪心-时间复杂度：O(n)，空间复杂度：O(1)
    // 贪心算法只能用于计算最大利润，计算的过程并不是实际的交易过程。
    class tsolution122_2 {
        public int maxProfit(int[] prices) {
            int ans = 0;
            int n = prices.length;
            for (int i = 1; i < n; ++i)
                ans += Math.max(0, prices[i] - prices[i - 1]);
            return ans;
        }
    }

    // 124.二叉树中的最大路径和
    // 路径 被定义为一条从树中任意节点出发，沿父节点-子节点连接，达到任意节点的序列。
    // 同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点，且不一定经过根节点。
    // 路径和 是路径中各节点值的总和。
    // 给你一个二叉树的根节点 root ，返回其 最大路径和 。
    // 提示：
    // 树中节点数目范围是 [1, 3 * 104]
    // -1000 <= Node.val <= 1000

    // 方法一：（自己写的）dfs-时间复杂度：O(n)，空间复杂度：O(n)
    class tsolution124 {
        int res = Integer.MIN_VALUE;

        public int maxPathSum(TreeNode root) {
            dfs(root);
            return res;
        }

        private int dfs(TreeNode root) {
            if (root == null)
                return 0;

            int leftVal = dfs(root.left);
            int rightVal = dfs(root.right);
            int rootVal = root.val;

            int leftRootVal = leftVal + rootVal;
            int rightRootVal = rightVal + rootVal;
            int leftRightRootVal = leftVal + rightVal + rootVal;

            int ans = Math.max(rootVal, Math.max(leftRootVal, rightRootVal));
            res = Math.max(res, Math.max(ans, leftRightRootVal));

            return ans;
        }
    }

    // 125.验证回文串
    // 如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后，短语正着读和反着读都一样。则可以认为该短语是一个 回文串 。
    // 字母和数字都属于字母数字字符。
    // 给你一个字符串 s，如果它是 回文串 ，返回 true ；否则，返回 false 。
    // 提示：
    // 1 <= s.length <= 2 * 105
    // s 仅由可打印的 ASCII 字符组成

    // 方法一：筛选 + 判断-时间复杂度：O(|s|)，空间复杂度：O(|s|)
    public boolean isPalindrome(String s) {
        // 处理字符串s，去除非字符和数字
        StringBuffer sgood = new StringBuffer();
        int length = s.length();
        for (int i = 0; i < length; i++) {
            char ch = s.charAt(i);
            if (Character.isLetterOrDigit(ch))
                sgood.append(Character.toLowerCase(ch));
        }
        int n = sgood.length();
        int left = 0, right = n - 1;
        while (left < right) {
            if (sgood.charAt(left) != sgood.charAt(right))
                return false;
            ++left;
            --right;
        }
        return true;
    }

    // 方法二：在原字符串上直接判断-时间复杂度：O(|s|)，空间复杂度：O(1)
    // 最好还是不要嵌套使用while，很多次重复判断 left < right
    public boolean isPalindrome2(String s) {
        int n = s.length();
        int left = 0, right = n - 1;
        while (left < right) {
            // 移动 left 指针，跳过非字符和数字
            while (left < right && !Character.isLetterOrDigit(s.charAt(left)))
                ++left;

            // 移动 right 指针，跳过非字符和数字
            while (left < right && !Character.isLetterOrDigit(s.charAt(right)))
                --right;

            if (left < right) {
                if (Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right)))
                    return false;
                ++left;
                --right;
            }
        }
        return true;
    }

    // 方法二：（自己写的）在原字符串上直接判断-时间复杂度：O(|s|)，空间复杂度：O(1)
    public boolean isPalindrome22(String s) {
        int left = 0, right = s.length() - 1;
        while (left < right) {
            char leftChar = s.charAt(left), rightChar = s.charAt(right);
            if (!Character.isLetterOrDigit(leftChar)) {
                left++;
                continue;
            }

            if (!Character.isLetterOrDigit(rightChar)) {
                right--;
                continue;
            }

            if (Character.toLowerCase(leftChar) != Character.toLowerCase(rightChar))
                return false;
            left++;
            right--;
        }
        return true;
    }

    // 127.单词接龙
    // 字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列
    // beginWord -> s1 -> s2 -> ... -> sk：
    // 每一对相邻的单词只差一个字母。
    // 对于 1 <= i <= k 时，每个 si 都在 wordList 中。注意， beginWord 不需要在 wordList 中。
    // sk == endWord
    // 给你两个单词 beginWord 和 endWord 和一个字典 wordList ，
    // 返回 从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。
    // 如果不存在这样的转换序列，返回 0 。
    // 提示：
    // 1 <= beginWord.length <= 10
    // endWord.length == beginWord.length
    // 1 <= wordList.length <= 5000
    // wordList[i].length == beginWord.length
    // beginWord、endWord 和 wordList[i] 由小写英文字母组成
    // beginWord != endWord
    // wordList 中的所有字符串 互不相同

    // 在字典（单词列表） wordList 中，从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列：
    // 序列中第一个单词是 beginWord 。
    // 序列中最后一个单词是 endWord 。
    // 每次转换只能改变一个字母。
    // 转换过程中的中间单词必须是字典 wordList 中的单词。
    // 给定两个长度相同但内容不同的单词 beginWord 和 endWord 和一个字典 wordList ，
    // 找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列，返回 0。
    // 提示：
    // 1 <= beginWord.length <= 10
    // endWord.length == beginWord.length
    // 1 <= wordList.length <= 5000
    // wordList[i].length == beginWord.length
    // beginWord、endWord 和 wordList[i] 由小写英文字母组成
    // beginWord != endWord
    // wordList 中的所有字符串 互不相同

    // 方法一：广度优先搜索 + 优化建图-时间复杂度：O(nC2)，C 为列表中单词的长度，空间复杂度：O(nC2)
    // 本题要求的是最短转换序列的长度，看到最短首先想到的就是广度优先搜索。
    class ssoltion_127_1 {
        int nodeNum = 0;
        Map<String, Integer> wordId = new HashMap<String, Integer>();// 词与索引的映射关系
        List<List<Integer>> edge = new ArrayList<List<Integer>>();// 图
        // 也可以直接Map<String, List<String>> edge; 这样不需要建立词与索引的映射关系，但效率会降低

        public int ladderLength(String beginWord, String endWord, List<String> wordList) {
            // 先给每一个单词标号，即给每个单词分配一个 id。
            // 创建一个由单词 word 到 id 对应的映射 wordId，并将 beginWord 与 wordList 中所有的单词都加入这个映射中。
            // 之后我们检查 endWord 是否在该映射内，若不存在，则输入无解。我们可以使用哈希表实现上面的映射关系。

            // 然后我们需要建图，依据朴素的思路，我们可以枚举每一对单词的组合，判断它们是否恰好相差一个字符，以判断这两个单词对应的节点是否能够相连。
            // 但是这样效率太低，我们可以优化建图。

            // 具体地，我们可以创建虚拟节点。
            // 对于单词 hit，我们创建三个虚拟节点 *it、h*t、hi*，并让 hit 向这三个虚拟节点分别连一条边即可。
            // 如果一个单词能够转化为 hit，那么该单词必然会连接到这三个虚拟节点之一。
            // 对于每一个单词，我们枚举它连接到的虚拟节点，把该单词对应的 id 与这些虚拟节点对应的 id 相连即可。
            for (String word : wordList)
                addEdge(word);
            addEdge(beginWord);
            if (!wordId.containsKey(endWord))
                return 0;

            // 最后我们将起点加入队列开始广度优先搜索，当搜索到终点时，我们就找到了最短路径的长度。
            // 注意因为添加了虚拟节点，所以我们得到的距离为实际最短路径长度的两倍。
            // 同时我们并未计算起点对答案的贡献，所以我们应当返回距离的一半再加一的结果。
            int[] dis = new int[nodeNum];// 起始节点到每个节点的最短路
            Arrays.fill(dis, Integer.MAX_VALUE);
            int beginId = wordId.get(beginWord), endId = wordId.get(endWord);
            dis[beginId] = 0;

            Queue<Integer> que = new LinkedList<Integer>();
            que.offer(beginId);
            while (!que.isEmpty()) {
                int x = que.poll();
                if (x == endId)// 第一个到终止节点
                    return dis[endId] / 2 + 1;

                for (int it : edge.get(x))
                    if (dis[it] == Integer.MAX_VALUE) {// 第一次访问时，入队，更新dis
                        dis[it] = dis[x] + 1;
                        que.offer(it);
                    }
            }
            return 0;
        }

        public void addEdge(String word) {
            addWord(word);
            int id1 = wordId.get(word);
            char[] array = word.toCharArray();
            int length = array.length;
            // 生成虚拟节点，并插入图
            for (int i = 0; i < length; ++i) {
                char tmp = array[i];
                array[i] = '*';
                String newWord = new String(array);
                addWord(newWord);
                int id2 = wordId.get(newWord);
                edge.get(id1).add(id2);
                edge.get(id2).add(id1);
                array[i] = tmp;
            }
        }

        public void addWord(String word) {
            if (!wordId.containsKey(word)) {
                wordId.put(word, nodeNum++);
                edge.add(new ArrayList<Integer>());
            }
        }
    }

    // 方法二：双向广度优先搜索
    // 根据给定字典构造的图可能会很大，而广度优先搜索的搜索空间大小依赖于每层节点的分支数量。
    // 假如每个节点的分支数量相同，搜索空间会随着层数的增长指数级的增加。
    // 如果使用两个同时进行的广搜可以有效地减少搜索空间。一边从 beginWord 开始，另一边从 endWord 开始。
    // 我们每次从两边各扩展一层节点，当发现某一时刻两边都访问过同一顶点时就停止搜索。
    class ssoltion_127_2 {
        Map<String, Integer> wordId = new HashMap<String, Integer>();
        List<List<Integer>> edge = new ArrayList<List<Integer>>();
        int nodeNum = 0;

        public int ladderLength(String beginWord, String endWord, List<String> wordList) {
            // 建图
            for (String word : wordList)
                addEdge(word);
            addEdge(beginWord);
            if (!wordId.containsKey(endWord))
                return 0;

            // 从起始节点开始bfs
            int[] disBegin = new int[nodeNum];
            Arrays.fill(disBegin, Integer.MAX_VALUE);
            int beginId = wordId.get(beginWord);
            disBegin[beginId] = 0;
            Queue<Integer> queBegin = new LinkedList<Integer>();
            queBegin.offer(beginId);

            // 从终止节点开始bfs
            int[] disEnd = new int[nodeNum];
            Arrays.fill(disEnd, Integer.MAX_VALUE);
            int endId = wordId.get(endWord);
            disEnd[endId] = 0;
            Queue<Integer> queEnd = new LinkedList<Integer>();
            queEnd.offer(endId);

            while (!queBegin.isEmpty() && !queEnd.isEmpty()) {
                // 从起始节点开始bfs
                int queBeginSize = queBegin.size();
                for (int i = 0; i < queBeginSize; ++i) {
                    int nodeBegin = queBegin.poll();
                    if (disEnd[nodeBegin] != Integer.MAX_VALUE)// 从终止节点开始的bfs已访问过该节点
                        return (disBegin[nodeBegin] + disEnd[nodeBegin]) / 2 + 1;

                    for (int it : edge.get(nodeBegin))
                        if (disBegin[it] == Integer.MAX_VALUE) {
                            disBegin[it] = disBegin[nodeBegin] + 1;
                            queBegin.offer(it);
                        }

                }

                // 从终止节点开始bfs
                int queEndSize = queEnd.size();
                for (int i = 0; i < queEndSize; ++i) {
                    int nodeEnd = queEnd.poll();
                    if (disBegin[nodeEnd] != Integer.MAX_VALUE)// 从起始节点开始的bfs已访问过该节点
                        return (disBegin[nodeEnd] + disEnd[nodeEnd]) / 2 + 1;

                    for (int it : edge.get(nodeEnd))
                        if (disEnd[it] == Integer.MAX_VALUE) {
                            disEnd[it] = disEnd[nodeEnd] + 1;
                            queEnd.offer(it);
                        }
                }
            }
            return 0;
        }

        public void addEdge(String word) {
            addWord(word);
            int id1 = wordId.get(word);
            char[] array = word.toCharArray();
            int length = array.length;
            for (int i = 0; i < length; ++i) {
                char tmp = array[i];
                array[i] = '*';
                String newWord = new String(array);
                addWord(newWord);
                int id2 = wordId.get(newWord);
                edge.get(id1).add(id2);
                edge.get(id2).add(id1);
                array[i] = tmp;
            }
        }

        public void addWord(String word) {
            if (!wordId.containsKey(word)) {
                wordId.put(word, nodeNum++);
                edge.add(new ArrayList<Integer>());
            }
        }
    }

    // 方法三：dfs + 记忆化搜索
    // 超时，最短路问题不推荐使用dfs

    // 128.最长连续序列
    // 给定一个未排序的整数数组 nums ，找出数字连续的最长序列（不要求序列元素在原数组中连续）的长度。
    // 请你设计并实现时间复杂度为 O(n) 的算法解决此问题。（人为的排序要nlogn）

    // 方法一：哈希表-时空复杂度：O(n)
    // 不含前驱，开始找它的后继
    public int longestConsecutive(int[] nums) {
        Set<Integer> num_set = new HashSet<>();// Set去重
        for (int num : nums)
            num_set.add(num);

        int longestStreak = 0;

        for (int num : num_set)// 重点：遍历集合内元素（去重），而不是数组内
            if (!num_set.contains(num - 1)) {// 不含前驱，开始找它的后继
                int currentNum = num;
                int currentStreak = 1;

                while (num_set.contains(currentNum + 1)) {
                    currentNum += 1;
                    currentStreak += 1;
                }

                longestStreak = Math.max(longestStreak, currentStreak);
            }

        return longestStreak;
    }

    // 130.被围绕的区域
    // 给你一个 m x n 的矩阵 board ，由若干字符 'X' 和 'O' ，
    // 找到所有被 'X' 围绕的区域，并将这些区域里所有的 'O' 用 'X' 填充。
    // 被围绕的区间不会存在于边界上，换句话说，任何边界上的 'O' 都不会被填充为 'X'。
    // 任何不在边界上，或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。
    // 如果两个元素在水平或垂直方向相邻，则称它们是“相连”的。
    // 提示：
    // m == board.length
    // n == board[i].length
    // 1 <= m, n <= 200
    // board[i][j] 为 'X' 或 'O'

    // 注意到题目解释中提到：任何边界上的 O 都不会被填充为 X。
    // 我们可以想到，所有的不被包围的 O 都直接或间接与边界上的 O 相连。
    // 我们可以利用这个性质判断 O 是否在边界上，具体地说：
    // 对于每一个边界上的 O，我们以它为起点，标记所有与它直接或间接相连的字母 O；
    // 最后我们遍历这个矩阵，对于每一个字母：
    // 如果该字母被标记过，则该字母为没有被字母 X 包围的字母 O，我们将其还原为字母 O；
    // 如果该字母没有被标记过，则该字母为被字母 X 包围的字母 O，我们将其修改为字母 X。
    // 精髓：改变遍历起点，使问题变简单

    // 方法一：dfs-时间复杂度：O(n×m)，空间复杂度：O(n×m)
    // 从边界上的 O 开始遍历，遍历修改分开进行，把标记过的字母 O 修改为字母 A。
    class ssolution_130_1 {
        int n, m;

        public void solve(char[][] board) {
            n = board.length;
            if (n == 0)
                return;

            m = board[0].length;
            for (int i = 0; i < n; i++) {
                dfs(board, i, 0);
                dfs(board, i, m - 1);
            }
            for (int i = 1; i < m - 1; i++) {
                dfs(board, 0, i);
                dfs(board, n - 1, i);
            }
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < m; j++) {
                    if (board[i][j] == 'A')
                        board[i][j] = 'O';
                    else if (board[i][j] == 'O')
                        board[i][j] = 'X';
                }
            }
        }

        public void dfs(char[][] board, int x, int y) {
            if (x < 0 || x >= n || y < 0 || y >= m || board[x][y] != 'O')
                return;

            board[x][y] = 'A';
            dfs(board, x + 1, y);
            dfs(board, x - 1, y);
            dfs(board, x, y + 1);
            dfs(board, x, y - 1);
        }
    }

    // 方法一：（自己写的）dfs-时间复杂度：O(n×m)，空间复杂度：O(n×m)
    // 遍历修改分开进行
    class ssoltion_130_11 {
        char[][] board;
        int m, n;
        boolean[][] visited;
        List<int[]> path = new ArrayList<>();
        boolean needModify;

        public void solve(char[][] board) {
            this.board = board;
            this.m = board.length;
            this.n = board[0].length;
            this.visited = new boolean[m][n];
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < n; j++) {
                    if (board[i][j] == 'X' || visited[i][j])
                        continue;

                    needModify = true;
                    path.clear();
                    dfs(i, j);

                    if (needModify)
                        for (int[] index : path)
                            board[index[0]][index[1]] = 'X';
                }
            }
        }

        void dfs(int i, int j) {
            if (i < 0 || i >= m || j < 0 || j >= n) {
                needModify = false;
                return;
            }
            if (board[i][j] == 'X')
                return;
            if (visited[i][j])
                return;

            // 能到这里，board[i][j] == 'O'
            visited[i][j] = true;
            path.add(new int[] { i, j });

            int[][] directions = new int[][] { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } };
            for (int[] direction : directions) {
                int newi = i + direction[0];
                int newj = j + direction[1];
                dfs(newi, newj);
            }
        }
    }

    // 方法二：bfs-时间复杂度：O(n×m)，空间复杂度：O(n×m)
    // 从边界上的 O 开始遍历，遍历修改分开进行，把标记过的字母 O 修改为字母 A。
    class ssolution_130_2 {
        int[] dx = { 1, -1, 0, 0 };
        int[] dy = { 0, 0, 1, -1 };

        public void solve(char[][] board) {
            int n = board.length;
            if (n == 0)
                return;

            int m = board[0].length;
            Queue<int[]> queue = new LinkedList<int[]>();
            for (int i = 0; i < n; i++) {
                if (board[i][0] == 'O') {
                    queue.offer(new int[] { i, 0 });
                    board[i][0] = 'A';
                }
                if (board[i][m - 1] == 'O') {
                    queue.offer(new int[] { i, m - 1 });
                    board[i][m - 1] = 'A';
                }
            }
            for (int i = 1; i < m - 1; i++) {
                if (board[0][i] == 'O') {
                    queue.offer(new int[] { 0, i });
                    board[0][i] = 'A';
                }
                if (board[n - 1][i] == 'O') {
                    queue.offer(new int[] { n - 1, i });
                    board[n - 1][i] = 'A';
                }
            }
            while (!queue.isEmpty()) {
                int[] cell = queue.poll();
                int x = cell[0], y = cell[1];
                for (int i = 0; i < 4; i++) {
                    int mx = x + dx[i], my = y + dy[i];
                    if (mx < 0 || my < 0 || mx >= n || my >= m || board[mx][my] != 'O')
                        continue;

                    queue.offer(new int[] { mx, my });
                    board[mx][my] = 'A';
                }
            }
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < m; j++) {
                    if (board[i][j] == 'A')
                        board[i][j] = 'O';
                    else if (board[i][j] == 'O')
                        board[i][j] = 'X';

                }
            }
        }
    }

    // 131.分割回文串
    // 给你一个字符串 s，请你将 s 分割成一些子串，使每个子串都是 回文串 。返回 s 所有可能的分割方案。
    // 回文串 是正着读和反着读都一样的字符串。
    // 提示：
    // 1 <= s.length <= 16
    // s 仅由小写英文字母组成

    // 方法一：回溯 + 动态规划预处理-时间复杂度：O(n2^n)，空间复杂度：O(n^2)
    class ssoltion_131_1 {
        boolean[][] f;
        List<List<String>> ret = new ArrayList<List<String>>();
        List<String> ans = new ArrayList<String>();
        int n;

        public List<List<String>> partition(String s) {
            n = s.length();
            f = new boolean[n][n];
            for (int i = 0; i < n; ++i)
                Arrays.fill(f[i], true);

            for (int i = n - 1; i >= 0; --i)
                for (int j = i + 1; j < n; ++j)
                    f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];

            dfs(s, 0);
            return ret;
        }

        public void dfs(String s, int i) {
            if (i == n) {
                ret.add(new ArrayList<String>(ans));
                return;
            }
            for (int j = i; j < n; ++j) {
                if (f[i][j]) {
                    ans.add(s.substring(i, j + 1));
                    dfs(s, j + 1);
                    ans.remove(ans.size() - 1);
                }
            }
        }
    }

    // 方法一：（自己写的）回溯 + 预处理-时间复杂度：O(n2^n)，空间复杂度：O(n^2)
    class ssoltion_131_11 {
        String s;
        int n;
        boolean[][] isPalindrome;
        List<String> ans = new ArrayList<>();
        List<List<String>> res = new ArrayList<>();

        public List<List<String>> partition(String s) {
            this.s = s;
            this.n = s.length();
            this.isPalindrome = new boolean[n][n];
            isPalindrome[n - 1][n - 1] = true;
            for (int i = 0; i < n - 1; i++) {
                check(i, i);
                check(i, i + 1);
            }
            dfs(0);
            return res;
        }

        private void check(int left, int right) {
            while (left >= 0 && right < n) {
                if (s.charAt(left) == s.charAt(right)) {
                    isPalindrome[left][right] = true;
                    left--;
                    right++;
                } else
                    break;
            }
        }

        private void dfs(int index) {
            if (index == n) {
                res.add(new ArrayList<>(ans));
                return;
            }
            for (int end = index; end < n; end++) {
                if (isPalindrome[index][end]) {
                    ans.add(s.substring(index, end + 1));
                    dfs(end + 1);
                    ans.remove(ans.size() - 1);
                }
            }
        }
    }

    // 方法二：回溯 + 记忆化搜索
    class ssolution_131_2 {
        int[][] f;
        List<List<String>> ret = new ArrayList<List<String>>();
        List<String> ans = new ArrayList<String>();
        int n;

        public List<List<String>> partition(String s) {
            n = s.length();
            f = new int[n][n];

            dfs(s, 0);
            return ret;
        }

        public void dfs(String s, int i) {
            if (i == n) {
                ret.add(new ArrayList<String>(ans));
                return;
            }
            for (int j = i; j < n; ++j) {
                if (isPalindrome(s, i, j) == 1) {
                    ans.add(s.substring(i, j + 1));
                    dfs(s, j + 1);
                    ans.remove(ans.size() - 1);
                }
            }
        }

        // 记忆化搜索中，f[i][j] = 0 表示未搜索，1 表示是回文串，-1 表示不是回文串
        public int isPalindrome(String s, int i, int j) {
            if (f[i][j] != 0)
                return f[i][j];

            if (i >= j)
                f[i][j] = 1;
            else if (s.charAt(i) == s.charAt(j))
                f[i][j] = isPalindrome(s, i + 1, j - 1);
            else
                f[i][j] = -1;

            return f[i][j];
        }
    }

    // 134.加油站
    // 在一条环路上有 n 个加油站，其中第 i 个加油站有汽油 gas[i] 升。
    // 你有一辆油箱容量无限的的汽车，从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。
    // 你从其中的一个加油站出发，开始时油箱为空。
    // 给定两个整数数组 gas 和 cost ，如果你可以绕环路行驶一周，则返回出发时加油站的编号，否则返回 -1 。
    // 如果存在解，则 保证 它是 唯一 的。
    // 提示:
    // gas.length == n
    // cost.length == n
    // 1 <= n <= 105
    // 0 <= gas[i], cost[i] <= 104

    // 方法一：一次遍历-时间复杂度：O(n)，空间复杂度：O(1)
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int n = gas.length;
        int i = 0;
        // 从头到尾遍历每个加油站，并且检查以该加油站为起点，能否行驶一周
        while (i < n) {
            int sumOfGas = 0;// 总共加的油
            int sumOfCost = 0;// 总共消费的油
            int cnt = 0;// 记录能走过几个站点
            // 退出循环的条件是走过所有的站点
            while (cnt < n) {
                int j = (i + cnt) % n;// 加油站是环形的
                sumOfGas += gas[j];
                sumOfCost += cost[j];
                if (sumOfCost > sumOfGas) // 如果这个站点发现油不够了
                    break;

                cnt++;// 这个站点满足情况
            }
            if (cnt == n) // 如果能环绕一圈
                return i;
            else // 从下一个站点开始检查（这里保证了时间复杂度是O(n)）
                i = i + cnt + 1;
        }
        return -1;// 所有加油站作为起点都不满足
    }

    // 方法一：（自己写的）一次遍历-时间复杂度：O(n)，空间复杂度：O(1)
    public int canCompleteCircuit11(int[] gas, int[] cost) {
        int n = gas.length;
        int sum = 0;// sum(gas)-sum(cost)
        int prev = 0;// 从起始点开始的sum记录
        int res = 0;// 最终答案，起始点
        for (int i = 0; i < n; i++) {
            int cur = gas[i] - cost[i];
            sum += cur;
            prev = prev + cur;
            if (prev < 0) {// 无法到下一站，选择下一站作为新的起始点
                res = i + 1;
                prev = 0;
            }
        }
        if (sum < 0 || res == n)// 一定不能绕环一周
            return -1;
        return res;
    }

    // 136.只出现一次的数字
    // 给你一个 非空 整数数组 nums ，除了某个元素只出现一次以外，其余每个元素均出现两次。找出那个只出现了一次的元素。
    // 你必须设计并实现线性时间复杂度的算法来解决此问题，且该算法只使用常量额外空间。
    // 提示：
    // 1 <= nums.length <= 3 * 104
    // -3 * 104 <= nums[i] <= 3 * 104
    // 除了某个元素只出现一次以外，其余每个元素均出现两次。

    // 方法一：位运算-时间复杂度：O(n)，空间复杂度：O(1)
    public int singleNumber(int[] nums) {
        int single = 0;
        for (int num : nums)
            single ^= num;

        return single;
    }

    // 138.复制芾随机指针的链表（剑指Offer 35.复杂链表的复制）
    // 给你一个长度为 n 的链表，每个节点包含一个额外增加的随机指针 random ，该指针可以指向链表中的任何节点或空节点。
    // 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成，其中每个新节点的值都设为其对应的原节点的值。
    // 新节点的 next 指针和 random 指针也都应指向复制链表中的新节点，并使原链表和复制链表中的这些指针能够表示相同的链表状态。
    // 复制链表中的指针都不应指向原链表中的节点 。
    // 例如，如果原链表中有 X 和 Y 两个节点，其中 X.random --> Y 。
    // 那么在复制链表中对应的两个节点 x 和 y ，同样有 x.random --> y 。
    // 返回复制链表的头节点。
    // 用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示：
    // val：一个表示 Node.val 的整数。
    // random_index：随机指针指向的节点索引（范围从 0 到 n-1）；如果不指向任何节点，则为  null 。
    // 你的代码 只 接受原链表的头节点 head 作为传入参数。
    // 提示：
    // 0 <= n <= 1000
    // -104 <= Node.val <= 104
    // Node.random 为 null 或指向链表中的节点。

    // 方法一：回溯（递归） + 哈希表-时间复杂度：O(n)，空间复杂度：O(n)
    // 本题中因为随机指针的存在，当我们拷贝节点时，「当前节点的随机指针指向的节点」可能还没创建，因此我们需要变换思路。
    // 一个可行方案是，我们利用回溯的方式，让每个节点的拷贝操作相互独立。
    // 对于当前节点，我们首先要进行拷贝，然后我们进行「当前节点的后继节点」和「当前节点的随机指针指向的节点」拷贝，
    // 拷贝完成后将创建的新节点的指针返回，即可完成当前节点的两指针的赋值。

    // 这里使用哈希表不用重写hashcode和equals方法（仅使用地址值，得到原链表和复制链表的映射）
    // 不重写hashcode方法就是返回对象的地址值，具有唯一性
    class ssoltion_138_1 {
        class Node {
            int val;
            Node next;
            Node random;

            public Node(int val) {
                this.val = val;
                this.next = null;
                this.random = null;
            }
        }

        Map<Node, Node> cachedNode = new HashMap<Node, Node>();

        public Node copyRandomList(Node head) {
            if (head == null) // next或random指向null时
                return null;

            if (!cachedNode.containsKey(head)) {// 复制next时进入，复制random时不进入，只返回对应复制节点
                Node headNew = new Node(head.val);
                cachedNode.put(head, headNew);
                headNew.next = copyRandomList(head.next);// 递归调用，根据next复制完整个链表后，才开始复制random
                headNew.random = copyRandomList(head.random);// 由尾及头复制random
            }
            return cachedNode.get(head);
        }
    }

    class ssoltion_138_11 {
        class Node {
            int val;
            Node next;
            Node random;

            public Node(int val) {
                this.val = val;
                this.next = null;
                this.random = null;
            }
        }

        public Node copyRandomList(Node head) {
            Map<Node, Node> srcDet = new HashMap<>();
            srcDet.put(null, null);// 方便找random指向
            Node dummyHead = new Node(-1);
            Node dummyNewHead = new Node(-1);
            dummyHead.next = head;
            Node newHead = dummyNewHead;
            while (head != null) {// 第一次遍历复制next，记录哈希映射
                Node newNode = new Node(head.val);
                newHead.next = newNode;
                srcDet.put(head, newNode);
                newHead = newHead.next;
                head = head.next;
            }

            head = dummyHead.next;
            newHead = dummyNewHead.next;
            while (head != null) {// 第二次遍历复制random
                newHead.random = srcDet.get(head.random);
                newHead = newHead.next;
                head = head.next;
            }
            return dummyNewHead.next;
        }
    }

    class ssoltion_138_2 {
        class Node {
            int val;
            Node next;
            Node random;

            public Node(int val) {
                this.val = val;
                this.next = null;
                this.random = null;
            }
        }

        public Node copyRandomList(Node head) {
            if (head == null)
                return null;

            for (Node node = head; node != null; node = node.next.next) {
                Node nodeNew = new Node(node.val);// 复制节点
                // 复制节点插入原链表
                nodeNew.next = node.next;
                node.next = nodeNew;
            }
            for (Node node = head; node != null; node = node.next.next) {
                Node nodeNew = node.next;
                // 复制random指向
                nodeNew.random = (node.random != null) ? node.random.next : null;
            }
            Node headNew = head.next;
            for (Node node = head; node != null; node = node.next) {
                Node nodeNew = node.next;
                // 还原两个链表的next指向
                node.next = node.next.next;
                nodeNew.next = (nodeNew.next != null) ? nodeNew.next.next : null;
            }
            return headNew;
        }

    }

    // 139.单词拆分
    // 给定一个非空字符串 s 和一个包含非空单词的列表 wordDict，判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
    // 说明：
    // 拆分时可以重复使用字典中的单词。
    // 你可以假设字典中没有重复的单词。
    // 注意：该题只需返回是否可以拆分，所以优先考虑动态规划，返回具体拆分方式，则考虑dfs
    // 提示：
    // 1 <= s.length <= 300
    // 1 <= wordDict.length <= 1000
    // 1 <= wordDict[i].length <= 20
    // s 和 wordDict[i] 仅有小写英文字母组成
    // wordDict 中的所有字符串 互不相同

    // 方法一：动态规划-时间复杂度：O(n2)，空间复杂度：O(n)
    // 定义 dp[i] 表示字符串 s 前 i 个字符组成的字符串 s[0..i−1] 是否能被空格拆分成若干个字典中出现的单词（可以是一个也可以是多个）。
    // 0 left right 两部分拼接
    public boolean wordBreak(String s, List<String> wordDict) {
        int n = s.length();
        Set<String> wordDictSet = new HashSet<>(wordDict);// List转Set方便判断contains
        boolean[] dp = new boolean[n + 1];
        dp[0] = true;// 0表示没有单词
        for (int right = 1; right <= n; right++)// 末尾为right（外层）
            for (int left = 0; left < right; left++)// 起始为left（left从头到尾，从尾到头，都可以）
                // 起始字符之前为true（此时接下来拆分的子串才有效），且接下来前闭后开的子串在字典内
                if (dp[left] && wordDictSet.contains(s.substring(left, right))) {// 只要能拆直接break，因为可能还存在其他拆分方法
                    dp[right] = true;
                    break;
                }

        return dp[n];// 字符串末尾为true则能拆分
    }

    // 140.单词拆分II
    // 给定一个字符串 s 和一个字符串字典 wordDict ，在字符串 s 中增加空格来构建一个句子，使得句子中所有的单词都在词典中。
    // 以任意顺序 返回所有这些可能的句子。
    // 注意：词典中的同一个单词可能在分段中被重复使用多次。
    // 提示：
    // 1 <= s.length <= 20
    // 1 <= wordDict.length <= 1000
    // 1 <= wordDict[i].length <= 10
    // s 和 wordDict[i] 仅有小写英文字母组成
    // wordDict 中所有字符串都 不同

    // 方法一：记忆化搜索
    class ssoltion_140_1 {
        public List<String> wordBreak(String s, List<String> wordDict) {
            Map<Integer, List<List<String>>> map = new HashMap<Integer, List<List<String>>>();
            List<List<String>> wordBreaks = backtrack(s, s.length(), new HashSet<String>(wordDict), 0, map);
            List<String> breakList = new LinkedList<String>();
            for (List<String> wordBreak : wordBreaks) {
                breakList.add(String.join(" ", wordBreak));
            }
            return breakList;
        }

        public List<List<String>> backtrack(String s, int length, Set<String> wordSet, int index,
                Map<Integer, List<List<String>>> map) {
            if (!map.containsKey(index)) {
                List<List<String>> wordBreaks = new LinkedList<List<String>>();
                if (index == length)
                    wordBreaks.add(new LinkedList<String>());

                for (int i = index + 1; i <= length; i++) {
                    String word = s.substring(index, i);
                    if (wordSet.contains(word)) {
                        List<List<String>> nextWordBreaks = backtrack(s, length, wordSet, i, map);
                        for (List<String> nextWordBreak : nextWordBreaks) {
                            LinkedList<String> wordBreak = new LinkedList<String>(nextWordBreak);
                            wordBreak.offerFirst(word);
                            wordBreaks.add(wordBreak);
                        }
                    }
                }
                map.put(index, wordBreaks);
            }
            return map.get(index);
        }
    }

    // 方法二：（自己写的）dfs 回溯 + 字典树（Design208.字典树）
    class ssoltion_140_2 {
        class Trie {// 内部类，字典树
            private Trie[] children;
            private boolean isEnd;
            private String word;

            public Trie() {
                children = new Trie[26];
                isEnd = false;
            }

            public void insert(String word) {
                Trie node = this;// 初始node指向自己（头节点）
                for (int i = 0; i < word.length(); i++) {// 逐个遍历每个字符
                    char ch = word.charAt(i);
                    int index = ch - 'a'; // 从a开始索引值为0
                    if (node.children[index] == null) // 若无则创建节点
                        node.children[index] = new Trie();

                    node = node.children[index];// 指向当前字符
                }
                node.isEnd = true;// 表示字符串结尾
                node.word = word;
            }
        }

        private Trie dTree = new Trie();
        private String str = null;

        StringBuilder ans = new StringBuilder();
        List<String> res = new ArrayList<>();

        public List<String> wordBreak(String s, List<String> wordDict) {
            str = s;
            for (String word : wordDict)
                dTree.insert(word);
            dfs(0);
            return res;
        }

        void dfs(int start) {
            if (start == str.length()) {// 可拆分
                ans.deleteCharAt(ans.length() - 1);
                res.add(ans.toString());
                return;
            }

            Trie node = dTree;
            for (int i = start; i < str.length(); i++) {
                char ch = str.charAt(i);
                int index = ch - 'a';
                if (node.children[index] == null)// 剪枝
                    break;
                node = node.children[index];
                if (node.isEnd) {
                    int ansStart = ans.length();
                    ans.append(node.word).append(" ");
                    dfs(i + 1);
                    ans.delete(ansStart, ans.length());
                }
                // 继续查找，e.g. cat cats
            }
        }
    }

    // 141.环形链表
    // 给你一个链表的头节点 head ，判断链表中是否有环。
    // 如果链表中有某个节点，可以通过连续跟踪 next 指针再次到达，则链表中存在环。
    // 为了表示给定链表中的环，评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。
    // 注意：pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
    // 如果链表中存在环 ，则返回 true 。 否则，返回 false 。
    // 提示：
    // 链表中节点的数目范围是 [0, 104]
    // -105 <= Node.val <= 105
    // pos 为 -1 或者链表中的一个 有效索引 。
    // 进阶：你能用 O(1)（即，常量）内存解决此问题吗？

    // 方法一：哈希表-时间复杂度：O(N)，空间复杂度：O(N)
    public boolean hasCycle(ListNode head) {
        Set<ListNode> seen = new HashSet<ListNode>();
        while (head != null) {
            if (!seen.add(head)) // 添加失败
                return true;

            head = head.next;
        }
        return false;
    }

    // 方法二：（自己写的）快慢指针-时间复杂度：O(N)，空间复杂度：O(1)
    // 「Floyd 判圈算法」（又称龟兔赛跑算法）
    // 我们定义两个指针，一快一满。慢指针每次只移动一步，而快指针每次移动两步。
    // 初始时，慢指针在位置 head，而快指针在位置 head.next。
    // 这样一来，如果在移动的过程中，快指针反过来追上慢指针，就说明该链表为环形链表。
    // 否则快指针将到达链表尾部，该链表不为环形链表。
    public boolean hasCycle2(ListNode head) {
        ListNode fast = head, slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (slow == fast)
                return true;
        }

        return false;
    }

    // 146.LRU缓存
    // 请你设计并实现一个满足  LRU (最近最少使用) 缓存 约束的数据结构。
    // 实现 LRUCache 类：
    // LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
    // int get(int key) 如果关键字 key 存在于缓存中，则返回关键字的值，否则返回 -1 。
    // void put(int key, int value) 如果关键字 key 已经存在，则变更其数据值 value ；
    // 如果不存在，则向缓存中插入该组 key-value 。
    // 如果插入操作导致关键字数量超过 capacity ，则应该 逐出 最久未使用的关键字。
    // 函数 get 和 put 必须以 O(1) 的平均时间复杂度运行。
    // 提示：
    // 1 <= capacity <= 3000
    // 0 <= key <= 10000
    // 0 <= value <= 105
    // 最多调用 2 * 105 次 get 和 put

    // 方法一：（自己写的）哈希表 + 双向链表-时间复杂度：O(1)，空间复杂度：O(capacity)
    // 在面试中这种设计题，面试官一般会期望读者能够自己实现一个简单的双向链表，而不是使用语言自带的、封装好的数据结构。
    // 在双向链表的实现中，使用一个伪头部（dummy head）和伪尾部（dummy tail）标记界限，
    // 这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。
    class LRUCache {
        class DLinkListNode {
            int key;
            int value;
            DLinkListNode prev;
            DLinkListNode next;

            DLinkListNode() {

            }

            DLinkListNode(int key, int value) {
                this.key = key;
                this.value = value;
            }
        }

        DLinkListNode dummyHead, dummyTail;
        Map<Integer, DLinkListNode> map;
        int capacity;

        public LRUCache(int capacity) {
            this.capacity = capacity;
            dummyHead = new DLinkListNode();
            dummyTail = new DLinkListNode();
            dummyHead.next = dummyTail;
            dummyTail.prev = dummyHead;
            map = new HashMap<>();
        }

        private void delete(DLinkListNode node) {
            DLinkListNode prev = node.prev;
            DLinkListNode next = node.next;
            prev.next = next;
            next.prev = prev;
        }

        private void push(DLinkListNode node) {
            DLinkListNode prev = dummyTail.prev;
            DLinkListNode next = dummyTail;
            node.prev = prev;
            node.next = next;
            prev.next = node;
            next.prev = node;
        }

        public int get(int key) {
            if (map.containsKey(key)) {
                DLinkListNode node = map.get(key);
                delete(node);
                push(node);
                return node.value;
            }
            return -1;
        }

        public void put(int key, int value) {
            if (map.containsKey(key)) {
                DLinkListNode node = map.get(key);
                node.value = value;
                delete(node);
                push(node);
                return;
            }

            DLinkListNode node = new DLinkListNode(key, value);
            if (capacity == map.size()) {
                DLinkListNode deleteNode = dummyHead.next;
                delete(deleteNode);
                map.remove(deleteNode.key);
            }
            push(node);
            map.put(key, node);
        }
    }

    // 方法二：自己写的lowb，用java自带的LinkedHashMap-时间复杂度：O(1)，空间复杂度：O(capacity)
    class LRUCache2 {
        Map<Integer, Integer> cache;
        int capacity;
        int size;

        public LRUCache2(int capacity) {
            this.capacity = capacity;
            cache = new LinkedHashMap<>(capacity);
        }

        public int get(int key) {
            if (cache.containsKey(key)) {
                Integer value = cache.get(key);
                cache.remove(key);
                cache.put(key, value);
                return value;
            } else
                return -1;
        }

        public void put(int key, int value) {
            if (cache.containsKey(key)) {
                cache.remove(key);
                cache.put(key, value);
            } else {
                ++size;
                if (size > capacity) {
                    Set<Integer> keySet = cache.keySet();
                    Iterator<Integer> keyIterator = keySet.iterator();
                    cache.remove(keyIterator.next()); // 一定不用判断hasNext()
                    --size;
                }
                cache.put(key, value);
            }
        }
    }

    // 148.排序链表
    // 给你链表的头结点 head ，请将其按 升序 排列并返回 排序后的链表 。
    // 进阶：
    // 你可以在 O(n log n) 时间复杂度和常数级空间复杂度下，对链表进行排序吗？
    // 提示：
    // 链表中节点的数目在范围 [0, 5 * 104] 内
    // -105 <= Node.val <= 105

    // 「147. 对链表进行插入排序」要求使用插入排序的方法对链表进行排序，插入排序的时间复杂度是 O(n^2)，其中 n 是链表的长度。
    // 这道题考虑时间复杂度更低的排序算法。题目的进阶问题要求达到 O(nlogn) 的时间复杂度和 O(1) 的空间复杂度，
    // 时间复杂度是 O(nlogn) 的排序算法包括归并排序、堆排序和快速排序（快速排序的最差时间复杂度是 O(n^2)，
    // 其中最适合链表的排序算法是归并排序。

    // 方法一：自顶向下归并排序-时间复杂度：O(nlogn)，其中 n 是链表的长度。空间复杂度：O(n)（拷贝数组，这里则是归并后的链表长度）
    // 对链表自顶向下归并排序的过程如下。（参考数组的归并排序）
    // 找到链表的中点，以中点为分界，将链表拆分成两个子链表。
    // 寻找链表的中点可以使用快慢指针的做法，快指针每次移动 2 步，慢指针每次移动 1 步，
    // 当快指针到达链表末尾时，慢指针指向的链表节点即为链表的中点。
    // 对两个子链表分别排序。
    // 将两个排序后的子链表合并，得到完整的排序后的链表。可以使用「21. 合并两个有序链表」的做法，将两个有序的子链表进行合并。
    // 上述过程可以通过递归实现。
    // 递归的终止条件是链表的节点个数小于或等于 1，即当链表为空或者链表只包含 1 个节点时，不需要对链表进行拆分和排序。
    public ListNode sortList(ListNode head) {
        return sortList(head, null);
    }

    public ListNode sortList(ListNode head, ListNode tail) {
        if (head == null)
            return head;

        // 由于特殊的拆分方式，拆分到仅剩两个元素时，直接去掉末尾元素
        // 不可能拆分成一个元素
        if (head.next == tail) {
            head.next = null;
            return head;
        }

        // 使用快慢指针找到中点进行拆分
        ListNode slow = head, fast = head;
        while (fast != tail) {
            slow = slow.next;
            fast = fast.next;
            if (fast != tail)
                fast = fast.next;
        }
        // 这里的mid可以理解成向上取整，而不是一般归并中的向下取整
        ListNode mid = slow;
        // 拆分（方式稍有变化，为了防止死循环）
        ListNode list1 = sortList(head, mid);
        ListNode list2 = sortList(mid, tail);
        // 合并
        ListNode sorted = merge(list1, list2);
        return sorted;
    }

    // 合并两个有序链表（迭代）
    public ListNode merge(ListNode head1, ListNode head2) {
        ListNode dummyHead = new ListNode(0);
        ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
        while (temp1 != null && temp2 != null) {
            if (temp1.val <= temp2.val) {
                temp.next = temp1;
                temp1 = temp1.next;
            } else {
                temp.next = temp2;
                temp2 = temp2.next;
            }
            temp = temp.next;
        }

        // 处理末尾
        if (temp1 != null)
            temp.next = temp1;
        else if (temp2 != null)
            temp.next = temp2;

        return dummyHead.next;
    }

    // 方法一：（自己写的）自顶向下归并排序-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 拆分更方便理解（一切参考数组的归并排序，链表的末尾看做是null，null是一个独立的节点）
    public ListNode sortList11(ListNode head) {
        return divide(head, null);
    }

    private ListNode divide(ListNode head, ListNode tail) {
        if (head == tail) {
            if (head != null)
                head.next = null;// 划分为单个节点时，next置为null，方便后续合并
            return head;
        }

        ListNode fast = head;
        ListNode slow = head;
        while (fast != tail && fast.next != tail) {
            fast = fast.next.next;
            slow = slow.next;
        }

        ListNode secondHead = slow.next;// 精髓所在，slow的next可能会被置为null，则提前获取后半部分的起始节点
        ListNode l1 = divide(head, slow);
        ListNode l2 = divide(secondHead, tail);

        return mergeTwoList(l1, l2);
    }

    private ListNode mergeTwoList(ListNode l1, ListNode l2) {
        ListNode dummyHead = new ListNode(-1);
        ListNode cur = dummyHead;
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                cur.next = l1;
                l1 = l1.next;
            } else {
                cur.next = l2;
                l2 = l2.next;
            }
            cur = cur.next;
        }

        if (l1 == null)
            cur.next = l2;
        if (l2 == null)
            cur.next = l1;

        return dummyHead.next;
    }

    // 方法一：（自己写的）自顶向下归并排序-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 区别在于舍弃使用tail，在自顶划分时就置尾结点为null，后续向下划分默认尾结点也为null
    class lt100Solition_148 {
        public ListNode sortList(ListNode head) {
            if (head == null)
                return head;
            return dfs(head);
        }

        private ListNode dfs(ListNode head) {
            if (head.next == null)
                return head;
            ListNode dummyHead = new ListNode(-1);
            dummyHead.next = head;
            ListNode fast = dummyHead, slow = dummyHead;
            while (fast != null && fast.next != null) {
                fast = fast.next.next;
                slow = slow.next;
            }
            ListNode secondListHead = slow.next;
            slow.next = null;

            ListNode list1 = dfs(head);
            ListNode list2 = dfs(secondListHead);
            return merge(list1, list2);
        }

        private ListNode merge(ListNode list1, ListNode list2) {
            ListNode dummyHead = new ListNode(-1);
            ListNode prev = dummyHead;
            while (list1 != null || list2 != null) {
                int list1Val = list1 == null ? Integer.MAX_VALUE : list1.val;
                int list2Val = list2 == null ? Integer.MAX_VALUE : list2.val;
                if (list1Val < list2Val) {
                    prev.next = list1;
                    list1 = list1.next;
                } else {
                    prev.next = list2;
                    list2 = list2.next;
                }
                prev = prev.next;
            }
            return dummyHead.next;
        }
    }

    // 方法二：自底向上归并排序-时间复杂度：O(nlogn)，空间复杂度：O(1)
    // 使用自底向上的方法实现归并排序，则可以达到 O(1) 的空间复杂度。
    // 首先求得链表的长度 length，然后将链表拆分成子链表进行合并。
    public ListNode sortList2(ListNode head) {
        if (head == null)
            return head;

        // 1. 首先从头向后遍历,统计链表长度
        int length = 0; // 用于统计链表长度
        ListNode node = head;
        while (node != null) {
            length++;
            node = node.next;
        }

        // 2. 初始化 引入dummynode
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;

        // 3. 每次将链表拆分成若干个长度为subLen的子链表 , 并按照每两个子链表一组进行合并
        // subLen每次左移一位（即sublen = sublen*2） PS:位运算对CPU来说效率更高
        for (int subLen = 1; subLen < length; subLen <<= 1) {
            ListNode prev = dummyHead;
            ListNode curr = dummyHead.next; // curr用于记录拆分链表的位置

            while (curr != null) { // 如果链表没有被拆完
                // 3.1 拆分subLen长度的链表1
                ListNode head_1 = curr; // 第一个链表的头 即 curr初始的位置
                for (int i = 1; i < subLen && curr != null && curr.next != null; i++) // 拆分出长度为subLen的链表1
                    curr = curr.next;

                // 3.2 拆分subLen长度的链表2
                ListNode head_2 = curr.next; // 第二个链表的头 即 链表1尾部的下一个位置
                curr.next = null; // 断开第一个链表和第二个链表的链接
                curr = head_2; // 第二个链表头 重新赋值给curr
                for (int i = 1; i < subLen && curr != null && curr.next != null; i++) // 再拆分出长度为subLen的链表2
                    curr = curr.next;

                // 3.3 再次断开 第二个链表最后的next的链接
                ListNode next = null;
                if (curr != null) {
                    next = curr.next; // next用于记录 拆分完两个链表的结束位置
                    curr.next = null; // 断开链接
                }

                // 3.4 合并两个subLen长度的有序链表
                ListNode merged = merge(head_1, head_2);
                prev.next = merged; // prev.next 指向排好序链表的头
                while (prev.next != null) // while循环 将prev移动到 subLen*2 的位置后去
                    prev = prev.next;

                curr = next; // next用于记录 拆分完两个链表的结束位置
            }
        }
        // 返回新排好序的链表
        return dummyHead.next;
    }

    // 合并两个有序链表（见方法一）
    // public ListNode merge(ListNode l1, ListNode l2)

    // 方法三：自己写的lowb堆排（和快排一样，空间复杂度都是O(n)）-时间复杂度：O(nlogn)，空间复杂度：O(n)
    public ListNode sortList3(ListNode head) {
        PriorityQueue<ListNode> pq = new PriorityQueue<>((node1, node2) -> node1.val - node2.val);// 小根堆
        while (head != null) {// 所有节点入队
            pq.offer(head);
            head = head.next;
        }
        ListNode dummyHead = new ListNode(-1);
        ListNode tail = dummyHead;
        while (!pq.isEmpty()) {
            ListNode cur = pq.poll();
            tail.next = cur;
            tail = cur;
        }
        tail.next = null;
        return dummyHead.next;
    }

    // 方法四：（自己写的）快排 api-时间复杂度：O(nlogn)，空间复杂度：O(n)
    public ListNode sortList4(ListNode head) {
        if (head == null)
            return null;

        List<ListNode> list = new ArrayList<>();
        while (head != null) {
            list.add(head);
            head = head.next;
        }
        int n = list.size();
        Collections.sort(list, (node1, node2) -> node1.val - node2.val);
        for (int i = 0; i < n - 1; i++)
            list.get(i).next = list.get(i + 1);

        list.get(n - 1).next = null;
        return list.get(0);
    }

    // 149.直线上最多的点数（程序员面试金典 面试题 16.14. 最佳直线，该题需要返回直线上最小的点）
    // 给你一个数组 points ，其中 points[i] = [xi, yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。
    // 提示：
    // 1 <= points.length <= 300
    // points[i].length == 2
    // -104 <= xi, yi <= 104
    // points 中的所有点 互不相同

    // 方法一：向量表示直线（哈希表）-时间复杂度：O(n2×logm)，其中 n 为点的数量，m 为横纵坐标差的最大值。
    // 单次最大公约数计算的时间复杂度是 O(logm)，空间复杂度：O(n)
    public int maxPoints(int[][] points) {
        int n = points.length;
        if (n <= 2) // 一定在一条直线上
            return n;

        int ret = 0;
        for (int i = 0; i < n; i++) {
            // 1. 就算剩下所有点都与点i共线，也不可能更优 2. 当前答案超过半数点，一定是最终答案
            if (ret >= n - i || ret > n / 2)
                break;

            Map<Integer, Integer> map = new HashMap<Integer, Integer>();
            for (int j = i + 1; j < n; j++) {
                int x = points[i][0] - points[j][0];
                int y = points[i][1] - points[j][1];
                if (x == 0)
                    y = 1;
                else if (y == 0)
                    x = 1;
                else {
                    if (y < 0) {
                        x = -x;
                        y = -y;
                    }
                    int gcdXY = gcd(Math.abs(x), Math.abs(y));
                    x /= gcdXY;
                    y /= gcdXY;
                }
                int key = y + x * 20001;
                map.put(key, map.getOrDefault(key, 0) + 1);
            }
            int maxn = 0;
            for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
                int num = entry.getValue();
                maxn = Math.max(maxn, num + 1);
            }
            ret = Math.max(ret, maxn);
        }
        return ret;
    }

    public int gcd(int a, int b) {
        return b != 0 ? gcd(b, a % b) : a;
    }

    // 方法二：直线的一般式方程（哈希表）（见 程序员面试金典 面试题 16.14. 最佳直线）-时间复杂度：O(n^2)，空间复杂度：O(n)

    // 方法三：向量表示直线（暴力枚举）-（见 程序员面试金典 面试题 16.14. 最佳直线）时间复杂度：O(n^3)，空间复杂度：O(1)

    // 150.逆波兰表达式求值
    // 给你一个字符串数组 tokens ，表示一个根据 逆波兰表示法 表示的算术表达式。
    // 请你计算该表达式。返回一个表示表达式值的整数。
    // 注意：
    // 有效的算符为 '+'、'-'、'*' 和 '/' 。
    // 每个操作数（运算对象）都可以是一个整数或者另一个表达式。
    // 两个整数之间的除法总是 向零截断 。
    // 表达式中不含除零运算。
    // 输入是一个根据逆波兰表示法表示的算术表达式。
    // 答案及所有中间计算结果可以用 32 位 整数表示。
    // 提示：
    // 1 <= tokens.length <= 104
    // tokens[i] 是一个算符（"+"、"-"、"*" 或 "/"），或是在范围 [-200, 200] 内的一个整数

    // 逆波兰表达式：
    // 逆波兰表达式是一种后缀表达式，所谓后缀就是指算符写在后面。
    // 平常使用的算式则是一种中缀表达式，如 ( 1 + 2 ) * ( 3 + 4 ) 。
    // 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。
    // 逆波兰表达式主要有以下两个优点：
    // 去掉括号后表达式无歧义，上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
    // 适合用栈操作运算：遇到数字则入栈；遇到算符则取出栈顶两个数字进行计算，并将结果压入栈中

    // 方法一：栈-时空复杂度：O(n)
    // 逆波兰表达式严格遵循「从左到右」的运算。计算逆波兰表达式的值时，使用一个栈存储操作数，从左到右遍历逆波兰表达式，进行如下操作：
    // 如果遇到操作数，则将操作数入栈；
    // 如果遇到运算符，则将两个操作数出栈，其中先出栈的是右操作数，后出栈的是左操作数，
    // 使用运算符对两个操作数进行运算，将运算得到的新操作数入栈。
    // 整个逆波兰表达式遍历完毕之后，栈内只有一个元素，该元素即为逆波兰表达式的值。
    public int evalRPN(String[] tokens) {
        Deque<Integer> stack = new LinkedList<Integer>();
        int n = tokens.length;
        for (int i = 0; i < n; i++) {
            String token = tokens[i];
            if (isNumber(token))
                stack.push(Integer.parseInt(token));
            else {
                int num2 = stack.pop();// 右操作数
                int num1 = stack.pop();// 左操作数
                switch (token) {
                    case "+":
                        stack.push(num1 + num2);
                        break;
                    case "-":
                        stack.push(num1 - num2);
                        break;
                    case "*":
                        stack.push(num1 * num2);
                        break;
                    case "/":
                        stack.push(num1 / num2);
                        break;
                    default:
                }
            }
        }
        return stack.pop();
    }

    public boolean isNumber(String token) {
        return !("+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token));
    }

    // 方法二：数组模拟栈-时空复杂度：O(n)
    public int evalRPN2(String[] tokens) {
        int n = tokens.length;
        int[] stack = new int[(n + 1) / 2];
        int index = -1;
        for (int i = 0; i < n; i++) {
            String token = tokens[i];
            switch (token) {
                case "+":
                    index--;
                    stack[index] += stack[index + 1];
                    break;
                case "-":
                    index--;
                    stack[index] -= stack[index + 1];
                    break;
                case "*":
                    index--;
                    stack[index] *= stack[index + 1];
                    break;
                case "/":
                    index--;
                    stack[index] /= stack[index + 1];
                    break;
                default:
                    index++;
                    stack[index] = Integer.parseInt(token);
            }
        }
        return stack[index];
    }

    // 152.乘积最大子数组
    // 给你一个整数数组 nums ，请你找出数组中乘积最大的连续子数组（该子数组中至少包含一个数字），并返回该子数组所对应的乘积。
    // 不能用前缀和，中间出现0，则后续前缀乘积都为0

    // 方法一：动态规划-时间复杂度为 O(n)，空间复杂度为 O(1)
    // 根据正负性进行分类讨论。
    // 考虑当前位置如果是一个负数的话，那么我们希望以它前一个位置结尾的某个段的积也是个负数，
    // 这样就可以负负得正，并且我们希望这个积尽可能「负得更多」，即尽可能小。
    // 如果当前位置是一个正数的话，我们更希望以它前一个位置结尾的某个段的积也是个正数，并且希望它尽可能地大。
    public int maxProduct(int[] nums) {
        // minF表示以第 i 个元素结尾的乘积最小子数组的乘积
        // maxF表示以第 i 个元素结尾的乘积最大子数组的乘积
        // 由于第 i 个状态只和第 i - 1 个状态相关，根据「滚动数组」思想，我们可以只用两个变量来维护 i - 1 时刻的状态
        int maxF = nums[0], minF = nums[0], ans = nums[0];
        int length = nums.length;
        for (int i = 1; i < length; ++i) {
            int num = nums[i];
            int mx = maxF, mn = minF;
            maxF = Math.max(mx * num, Math.max(num, mn * num));// 尽可能大
            minF = Math.min(mx * num, Math.min(num, mn * num));// 尽可能小
            ans = Math.max(maxF, ans);
        }
        return ans;
    }

    // 方法二：分治（线段树）-时间复杂度：O(n)，空间复杂度：O(logn)
    // 需同时维护正负的最大值

    // 155.最小栈
    // 设计一个支持 push ，pop ，top 操作，并能在常数时间内检索到最小元素的栈。
    // 实现 MinStack 类:
    // MinStack() 初始化堆栈对象。
    // void push(int val) 将元素val推入堆栈。
    // void pop() 删除堆栈顶部的元素。
    // int top() 获取堆栈顶部的元素。
    // int getMin() 获取堆栈中的最小元素。
    // 提示：
    // -231 <= val <= 231 - 1
    // pop、top 和 getMin 操作总是在 非空栈 上调用
    // push, pop, top, and getMin最多被调用 3 * 104 次

    // 方法一：辅助栈-时间复杂度：O(n)，空间复杂度：O(n)
    class MinStack {
        Deque<Integer> stk;
        Deque<Integer> minstk;

        public MinStack() {
            stk = new ArrayDeque<>();
            minstk = new ArrayDeque<>();
        }

        public void push(int val) {
            if (stk.isEmpty()) {
                stk.push(val);
                minstk.push(val);
            } else {
                stk.push(val);
                minstk.push(Math.min(minstk.peek(), val));
            }
        }

        public void pop() {
            stk.pop();
            minstk.pop();
        }

        public int top() {
            return stk.peek();
        }

        public int getMin() {
            return minstk.peek();
        }
    }

    // 160.相交链表
    // 给你两个单链表的头节点 headA 和 headB ，请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点，返回 null 。
    // 图示两个链表在节点 c1 开始相交：
    // 题目数据 保证 整个链式结构中不存在环。
    // 注意，函数返回结果后，链表必须保持其原始结构 。
    // 自定义评测：
    // 评测系统 的输入如下（你设计的程序 不适用 此输入）：
    // intersectVal - 相交的起始节点的值。如果不存在相交节点，这一值为 0
    // listA - 第一个链表
    // listB - 第二个链表
    // skipA - 在 listA 中（从头节点开始）跳到交叉节点的节点数
    // skipB - 在 listB 中（从头节点开始）跳到交叉节点的节点数
    // 评测系统将根据这些输入创建链式数据结构，并将两个头节点 headA 和 headB 传递给你的程序。
    // 如果程序能够正确返回相交节点，那么你的解决方案将被 视作正确答案 。
    // 提示：
    // listA 中节点数目为 m
    // listB 中节点数目为 n
    // 0 <= m, n <= 3 * 104
    // 1 <= Node.val <= 105
    // 0 <= skipA <= m
    // 0 <= skipB <= n
    // 如果 listA 和 listB 没有交点，intersectVal 为 0
    // 如果 listA 和 listB 有交点，intersectVal == listA[skipA] == listB[skipB]
    // 进阶：你能否设计一个时间复杂度 O(n) 、仅用 O(1) 内存的解决方案？

    // 方法一：哈希集合-时间复杂度：O(m+n)，空间复杂度：O(m)
    // 判断两个链表是否相交，可以使用哈希集合存储链表节点。
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        Set<ListNode> visited = new HashSet<ListNode>();
        ListNode temp = headA;
        // 首先遍历链表 headA，并将链表 headA 中的每个节点加入哈希集合中。
        while (temp != null) {
            visited.add(temp);
            temp = temp.next;
        }
        temp = headB;
        // 然后遍历链表 headB，对于遍历到的每个节点，判断该节点是否在哈希集合中
        while (temp != null) {
            if (visited.contains(temp))
                return temp;

            temp = temp.next;
        }
        return null;
    }

    // 方法二：双指针-时间复杂度：O(m+n)，空间复杂度：O(1)
    // 精髓在于不相交链表可以看做同时指向null，因此不用做特殊判断
    // 链表 headA 和 headB 的长度分别是 m 和 n。
    // 假设链表 headA 的不相交部分有 a 个节点，链表 headB 的不相交部分有 b 个节点，两个链表相交的部分有 c 个节点，
    // 则有 a+c=m，b+c=n。
    // 如果 a=b，则两个指针会同时到达两个链表相交的节点，此时返回相交的节点；
    // 如果 a!=b，则指针 pA 会遍历完链表 headA，指针 pB 会遍历完链表 headB，两个指针不会同时到达链表的尾节点，
    // 然后指针 pA 移到链表 headB 的头节点，指针 pB 移到链表 headA 的头节点，然后两个指针继续移动，
    // 在指针 pA 移动了 a+c+b 次、指针 pB 移动了b+c+a 次之后，两个指针会同时到达两个链表相交的节点，
    // 该节点也是两个指针第一次同时指向的节点，此时返回相交的节点。
    // 如果两链表不相交，则两指针最后会同时指向null，返回null表示不相交
    public ListNode getIntersectionNode2(ListNode headA, ListNode headB) {
        // 一定不相交
        if (headA == null || headB == null)
            return null;

        ListNode pA = headA, pB = headB;
        while (pA != pB) {
            pA = pA == null ? headB : pA.next;
            pB = pB == null ? headA : pB.next;
        }
        return pA;
    }

    // 162.寻找峰值
    // 峰值元素是指其值严格大于左右相邻值的元素。
    // 给你一个整数数组 nums，找到峰值元素并返回其索引。数组可能包含多个峰值，在这种情况下，返回 任何一个峰值 所在位置即可。
    // 你可以假设 nums[-1] = nums[n] = -∞ 。
    // 你必须实现时间复杂度为 O(log n) 的算法来解决此问题。
    // 提示：
    // 1 <= nums.length <= 1000
    // -231 <= nums[i] <= 231 - 1
    // 对于所有有效的 i 都有 nums[i] != nums[i + 1]

    // 方法一：二分查找-时间复杂度：O(logn)，空间复杂度：O(1)
    public int findPeakElement(int[] nums) {
        int n = nums.length;
        int left = 0, right = n - 1, ans = -1;
        while (left <= right) {
            int mid = (left + right) / 2;
            if (compare(nums, mid - 1, mid) < 0 && compare(nums, mid, mid + 1) > 0) {
                ans = mid;
                break;
            }
            if (compare(nums, mid, mid + 1) < 0)
                left = mid + 1;
            else
                right = mid - 1;
        }
        return ans;
    }

    // 辅助函数，输入下标 i，返回一个二元组 (0/1, nums[i])
    // 方便处理 nums[-1] 以及 nums[n] 的边界情况
    public int[] get(int[] nums, int idx) {
        if (idx == -1 || idx == nums.length)
            return new int[] { 0, 0 };

        return new int[] { 1, nums[idx] };
    }

    // 比大小。返回正值 nums[idx1]更大，返回负值 nums[idx2]更大，返回0 则相等
    public int compare(int[] nums, int idx1, int idx2) {
        int[] num1 = get(nums, idx1);
        int[] num2 = get(nums, idx2);
        if (num1[0] != num2[0])
            return num1[0] > num2[0] ? 1 : -1;

        if (num1[1] == num2[1])
            return 0;

        return num1[1] > num2[1] ? 1 : -1;
    }

    // 方法一：（自己写的）二分查找-时间复杂度：O(logn)，空间复杂度：O(1)
    public int findPeakElement11(int[] nums) {
        int n = nums.length;
        if (n == 1)
            return 0;

        int left = 0, right = n - 1;
        while (left <= right) {
            int mid = (left + right) / 2;
            if (mid == 0 && nums[0] > nums[1])
                return 0;

            if (mid == n - 1 && nums[n - 1] > nums[n - 2])
                return n - 1;

            if (nums[mid] < nums[mid + 1])
                left = mid + 1;
            else if (nums[mid - 1] > nums[mid])
                right = mid - 1;
            else
                return mid;
        }
        return left;
    }

    // 163.缺失的区间
    // 给定一个排序的整数数组 nums ，其中元素的范围在 闭区间 [lower, upper] 当中，返回不包含在数组中的缺失区间。
    // 示例：
    // 输入: nums = [0, 1, 3, 50, 75], lower = 0 和 upper = 99,
    // 输出: ["2", "4->49", "51->74", "76->99"]

    // 方法一：模拟 一次遍历-时间复杂度：O(n)，空间复杂度：O(1)
    public List<String> findMissingRanges(int[] nums, int lower, int upper) {
        List<String> ans = new ArrayList<>();
        int pre = lower - 1;
        for (int num : nums) {
            add(ans, pre + 1, num - 1);
            pre = num;
        }
        add(ans, pre + 1, upper);
        return ans;
    }

    public void add(List<String> ans, int a, int b) {
        if (b >= a)
            ans.add(b > a ? a + "->" + b : a + "");
    }

    // 166.分数到小数
    // 给定两个整数，分别表示分数的分子 numerator 和分母 denominator，以 字符串形式返回小数 。
    // 如果小数部分为循环小数，则将循环的部分括在括号内。
    // 如果存在多个答案，只需返回 任意一个 。
    // 对于所有给定的输入，保证 答案字符串的长度小于 104 。
    // 提示：
    // -231 <= numerator, denominator <= 231 - 1
    // denominator != 0

    // 方法一：长除法-时间复杂度：O(n)，空间复杂度：O(n)。n为答案字符串长度。
    // 将分数转成整数或小数，做法是计算分子和分母相除的结果。可能的结果有三种：整数、有限小数、无限循环小数。
    // 1. 如果分子可以被分母整除，则结果是整数，将分子除以分母的商以字符串的形式返回即可。
    // 2. 如果分子不能被分母整除，则结果是有限小数或无限循环小数，需要通过模拟长除法的方式计算结果。
    // 为了方便处理，首先根据分子和分母的正负决定结果的正负（注意此时分子和分母都不为 0），然后将分子和分母都转成正数，再计算长除法。
    // 计算长除法时，首先计算结果的整数部分，将以下部分依次拼接到结果中：
    // 如果结果是负数则将负号拼接到结果中，如果结果是正数则跳过这一步；
    // 2.1. 将整数部分拼接到结果中；
    // 2.2. 将小数点拼接到结果中。
    // 2.2. 完成上述拼接之后，根据余数计算小数部分。
    // 计算小数部分时，每次将余数乘以 10，然后计算小数的下一位数字，并得到新的余数。重复上述操作直到余数变成 0 或者找到循环节。
    // 如果余数变成 0，则结果是有限小数，将小数部分拼接到结果中。
    // 如果找到循环节，则找到循环节的开始位置和结束位置并加上括号，然后将小数部分拼接到结果中。
    // 如何判断是否找到循环节？
    // 注意到对于相同的余数，计算得到的小数的下一位数字一定是相同的，
    // 因此如果计算过程中发现某一位的余数在之前已经出现过，则为找到循环节。
    // 为了记录每个余数是否已经出现过，需要使用哈希表存储每个余数在小数部分第一次出现的下标。
    public String fractionToDecimal(int numerator, int denominator) {
        // 为了防止计算过程中产生溢出，需要将分子和分母转成 64 位整数表示。
        long numeratorLong = (long) numerator;
        long denominatorLong = (long) denominator;
        // 1. 分子可以被分母整除
        if (numeratorLong % denominatorLong == 0)
            return String.valueOf(numeratorLong / denominatorLong);

        // 2. 分子不能被分母整除
        StringBuffer sb = new StringBuffer();
        if (numeratorLong < 0 ^ denominatorLong < 0)
            sb.append('-');

        // 2.1. 整数部分
        numeratorLong = Math.abs(numeratorLong);
        denominatorLong = Math.abs(denominatorLong);
        long integerPart = numeratorLong / denominatorLong;
        sb.append(integerPart);
        // 2.2. 小数点
        sb.append('.');

        // 2.3. 小数部分
        StringBuffer fractionPart = new StringBuffer();
        Map<Long, Integer> remainderIndexMap = new HashMap<Long, Integer>();
        long remainder = numeratorLong % denominatorLong;
        int index = 0;
        // 无循环，或某一位的余数在之前已经出现过，则为找到循环节
        while (remainder != 0 && !remainderIndexMap.containsKey(remainder)) {
            remainderIndexMap.put(remainder, index);
            remainder *= 10;
            fractionPart.append(remainder / denominatorLong);
            remainder %= denominatorLong;
            index++;
        }
        if (remainder != 0) { // 有循环节
            int insertIndex = remainderIndexMap.get(remainder);
            fractionPart.insert(insertIndex, '(');
            fractionPart.append(')');
        }
        sb.append(fractionPart.toString());

        return sb.toString();
    }

    // 169.多数元素
    // 给定一个大小为 n 的数组，找到其中的多数元素。多数元素是指在数组中出现次数 大于 ⌊ n/2 ⌋ 的元素。
    // 你可以假设数组是非空的，并且给定的数组总是存在多数元素。
    // 进阶：
    // 尝试设计时间复杂度为 O(n)、空间复杂度为 O(1) 的算法解决此问题。

    // 方法一：哈希表-时间复杂度：O(n)，空间复杂度：O(n)

    // 方法二：排序（下标为 ⌊ n/2 ⌋ 的元素一定是众数）-时间复杂度：O(nlogn)，空间复杂度：O(logn)
    // 如果使用语言自带的排序算法，需要使用 O(logn) 的栈空间。
    // 如果自己编写堆排序，则只需要使用 O(1) 的额外空间。

    // 方法三：随机化-时间复杂度为 O(n)，空间复杂度：O(1)
    // 随机挑选一个下标，检查它是否是众数，如果是就返回，否则继续随机挑选。
    private int randRange(Random rand, int min, int max) {
        return rand.nextInt(max - min) + min;
    }

    private int countOccurences(int[] nums, int num) {
        int count = 0;
        for (int i = 0; i < nums.length; i++)
            if (nums[i] == num)
                count++;

        return count;
    }

    public int majorityElement3(int[] nums) {
        Random rand = new Random();
        int majorityCount = nums.length / 2;// 多数元素至少出现的次数

        while (true) {
            int candidate = nums[randRange(rand, 0, nums.length)];// 随机取一个数
            if (countOccurences(nums, candidate) > majorityCount) // 验证该数出现的次数
                return candidate;
        }
    }

    // 方法四：分治-时间复杂度：O(nlogn)，空间复杂度：O(logn)
    // 使用经典的分治算法递归求解，直到所有的子问题都是长度为 1 的数组。
    // 长度为 1 的子数组中唯一的数显然是众数，直接返回即可。
    // 如果回溯后某区间的长度大于 1，我们必须将左右子区间的值合并。
    // 如果它们的众数相同，那么显然这一段区间的众数是它们相同的值。
    // 否则，我们需要比较两个众数在整个区间内出现的次数来决定该区间的众数。
    private int countInRange(int[] nums, int num, int lo, int hi) {
        int count = 0;
        for (int i = lo; i <= hi; i++)
            if (nums[i] == num)
                count++;

        return count;
    }

    private int majorityElementRec(int[] nums, int lo, int hi) {
        // 长度为 1 的子数组中唯一的数显然是众数，直接返回即可。
        if (lo == hi)
            return nums[lo];

        // 将区间一分为二
        int mid = (hi + lo) / 2;
        int left = majorityElementRec(nums, lo, mid);
        int right = majorityElementRec(nums, mid + 1, hi);

        // 左右子数组众数一致
        if (left == right)
            return left;

        // 左右子数组众数不一致，返回更多者
        int leftCount = countInRange(nums, left, lo, hi);
        int rightCount = countInRange(nums, right, lo, hi);

        return leftCount > rightCount ? left : right;
    }

    public int majorityElement4(int[] nums) {
        return majorityElementRec(nums, 0, nums.length - 1);
    }

    // 方法五：Boyer-Moore 摩尔投票算法-时间复杂度为 O(n)，空间复杂度：O(1)
    // 如果我们把众数记为 +1，把其他数记为 −1，将它们全部加起来，显然和大于 0，从结果本身我们可以看出众数比其他数多。
    // 我们维护一个候选众数 candidate 和它出现的次数 count。初始时 candidate 可以为任意值，count 为 0；
    // 我们遍历数组 nums 中的所有元素，对于每个元素 x，在判断 x 之前，如果 count 的值为 0，我们先将 x 的值赋予 candidate，
    // 随后我们判断 x：
    // 如果 x 与 candidate 相等，那么计数器 count 的值增加 1；
    // 如果 x 与 candidate 不等，那么计数器 count 的值减少 1。
    // 在遍历完成后，candidate 即为整个数组的众数。
    public int majorityElement5(int[] nums) {
        int count = 0;
        int candidate = -1;

        for (int num : nums) {
            if (count == 0)
                candidate = num;

            count += (num == candidate) ? 1 : -1;
        }

        return candidate;
    }

    // 171.Excel表列序号
    // 给你一个字符串 columnTitle ，表示 Excel 表格中的列名称。返回 该列名称对应的列序号 。
    // 提示：
    // 1 <= columnTitle.length <= 7
    // columnTitle 仅由大写英文组成
    // columnTitle 在范围 ["A", "FXSHRXW"] 内

    // 方法一：进制转换-时间复杂度：O(n)，空间复杂度：O(1)
    public int titleToNumber(String columnTitle) {
        int number = 0;
        int multiple = 1;
        for (int i = columnTitle.length() - 1; i >= 0; i--) {
            int k = columnTitle.charAt(i) - 'A' + 1;
            number += k * multiple;
            multiple *= 26;
        }
        return number;
    }

    // 方法一：（自己写的）进制转换-时间复杂度：O(n)，空间复杂度：O(1)
    public int titleToNumber11(String columnTitle) {
        int res = 0;
        char[] charArr = columnTitle.toCharArray();

        for (int i = charArr.length - 1, exp = 0; i >= 0; i--, exp++)
            res += (charArr[i] - 'A' + 1) * Math.pow(26, exp);

        return res;
    }

    // 172.阶乘后的零
    // 给定一个整数 n ，返回 n! 结果中尾随零的数量。
    // 提示 
    // n! = n * (n - 1) * (n - 2) * ... * 3 * 2 * 1
    // 0 <= n <= 104
    // 进阶：你可以设计并实现对数时间复杂度的算法来解决此问题吗？

    // 方法一：数学-时间复杂度：O(n)，空间复杂度：O(1)
    public int trailingZeroes(int n) {
        int ans = 0;
        for (int i = 5; i <= n; i += 5)
            for (int x = i; x % 5 == 0; x /= 5)
                ++ans;

        return ans;
    }

    // 方法二：优化计算-时间复杂度：O(logn)，空间复杂度：O(1)
    public int trailingZeroes2(int n) {
        int ans = 0;
        while (n != 0) {// 依次计算只含5, 25, 125,...的数的个数
            n /= 5;
            ans += n;
        }
        return ans;
    }

    // 179.最大数
    // 给定一组非负整数 nums，重新排列每个数的顺序（每个数不可拆分）使之组成一个最大的整数。
    // 注意：输出结果可能非常大，所以你需要返回一个字符串而不是整数。
    // 提示：
    // 1 <= nums.length <= 100
    // 0 <= nums[i] <= 109

    // 方法一：排序-时间复杂度：O(nlogn·logm)，其中 n 是给定序列的长度，m 是 32 位整数的最大值
    // 排序比较函数的时间复杂度为 O(logm)，共需要进行 O(nlogn) 次比较
    // 对字符串序列进行拼接，时间复杂度为 O(nlogm)
    // 空间复杂度：O(logn)

    // 令 s(x) 表示大于非负整数 x 的最小的十的整次幂
    // 无需真正比较两种拼接结果大小，而是通过 (y * sx + x -( x * sy + y)) 比较
    public String largestNumber(int[] nums) {
        int n = nums.length;
        // 转换成包装类型，以便传入 Comparator 对象（此处为 lambda 表达式）
        Integer[] numsArr = new Integer[n];
        for (int i = 0; i < n; i++)
            numsArr[i] = nums[i];

        Arrays.sort(numsArr, (x, y) -> {
            long sx = 10, sy = 10;
            while (sx <= x)
                sx *= 10;
            while (sy <= y)
                sy *= 10;
            return (int) (y * sx + x - (x * sy + y));// y在前x在后，x在前y在后
        });

        if (numsArr[0] == 0)// [0,0] -> 0 而不是 00
            return "0";

        StringBuilder ret = new StringBuilder();
        for (int num : numsArr)
            ret.append(num);
        return ret.toString();
    }

    // 方法一：（自己写的）排序-时间复杂度：O(nlogn·logm)，其中 n 是给定序列的长度，m 是 32 位整数的最大值
    // 排序比较函数的时间复杂度为 O(logm)，共需要进行 O(nlogn) 次比较
    // 对字符串序列进行拼接，时间复杂度为 O(nlogm)
    // 空间复杂度：O(logn)
    public String largestNumber11(int[] nums) {
        List<String> list = new ArrayList<>();
        for (int num : nums)
            list.add(String.valueOf(num));

        Collections.sort(list, (str1, str2) -> (str2 + str1).compareTo(str1 + str2));

        if ("0".equals(list.get(0)))
            return "0";

        StringBuilder res = new StringBuilder();
        for (String str : list)
            res.append(str);
        return res.toString();
    }

    // 189.轮转数组
    // 给定一个整数数组 nums，将数组中的元素向右轮转 k 个位置，其中 k 是非负数。
    // 提示：
    // 1 <= nums.length <= 105
    // -231 <= nums[i] <= 231 - 1
    // 0 <= k <= 105
    // 进阶：
    // 尽可能想出更多的解决方案，至少有 三种 不同的方法可以解决这个问题。
    // 你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗？

    // 方法一：使用额外的数组-时间复杂度：O(n)，空间复杂度：O(n)
    public void rotate(int[] nums, int k) {
        int n = nums.length;
        int[] newArr = new int[n];
        for (int i = 0; i < n; ++i)
            newArr[(i + k) % n] = nums[i];

        System.arraycopy(newArr, 0, nums, 0, n);
    }

    // 方法二：环状替换-时间复杂度：O(n)，空间复杂度：O(1)
    // 容易发现，当回到初始位置 0 时，有些数字可能还没有遍历到，此时我们应该从下一个数字开始重复的过程，可是这个时候怎么才算遍历结束呢？
    // 我们不妨先考虑这样一个问题：从 0 开始不断遍历，最终回到起点 0 的过程中，我们遍历了多少个元素？
    // 由于最终回到了起点，故该过程恰好走了整数数量的圈，不妨设为 a 圈；再设该过程总共遍历了 b 个元素。
    // 因此，我们有 an=bk，即 an 一定为 n,k 的公倍数。
    // 又因为我们在第一次回到起点时就结束，因此 a 要尽可能小，故 an 就是 n,k 的最小公倍数 lcm(n,k)，
    // 因此 b 就为 lcm(n,k)/k = gcd(n, k)，即 n,k 的最大公约数。
    class ssoltion_189_2 {
        public void rotate(int[] nums, int k) {
            int n = nums.length;
            k = k % n;
            int count = gcd(n, k);
            for (int start = 0; start < count; ++start) {
                int current = start;
                int prev = nums[start];
                do {
                    int next = (current + k) % n;
                    int temp = nums[next];
                    nums[next] = prev;
                    prev = temp;
                    current = next;
                } while (start != current);
            }
        }

        public int gcd(int x, int y) {
            return y > 0 ? gcd(y, x % y) : x;
        }
    }

    // 方法三：数组翻转-时间复杂度：O(n)，空间复杂度：O(1)
    // 该方法基于如下的事实：当我们将数组的元素向右移动 k 次后，尾部 k mod n 个元素会移动至数组头部，其余元素向后移动 k mod n 个位置。
    // 该方法为数组的翻转：我们可以先将所有元素翻转，这样尾部的 k mod n 个元素就被移至数组头部，
    // 然后我们再翻转 [0,k mod n−1] 区间的元素和 [k mod n,n−1] 区间的元素即能得到最后的答案。
    public void rotate3(int[] nums, int k) {
        k %= nums.length;
        reverse(nums, 0, nums.length - 1);
        reverse(nums, 0, k - 1);
        reverse(nums, k, nums.length - 1);
    }

    public void reverse(int[] nums, int start, int end) {
        while (start < end) {
            int temp = nums[start];
            nums[start] = nums[end];
            nums[end] = temp;
            start += 1;
            end -= 1;
        }
    }

    // 190.颠倒二进制位
    // 颠倒给定的 32 位无符号整数的二进制位。
    // 提示：
    // 请注意，在某些语言（如 Java）中，没有无符号整数类型。
    // 在这种情况下，输入和输出都将被指定为有符号整数类型，并且不应影响您的实现，
    // 因为无论整数是有符号的还是无符号的，其内部的二进制表示形式都是相同的。
    // 在 Java 中，编译器使用二进制补码记法来表示有符号整数。
    // 因此，在 示例 2 中，输入表示有符号整数 -3，输出表示有符号整数 -1073741825。
    // 提示：
    // 输入是一个长度为 32 的二进制字符串
    // 进阶: 如果多次调用这个函数，你将如何优化你的算法？

    // 方法一：逐位颠倒-时间复杂度：O(logn)，空间复杂度：O(1)
    // 需要注意的是，在某些语言（如 Java）中，没有无符号整数类型，因此对 n 的右移操作应使用逻辑右移。
    public int reverseBits(int n) {
        int rev = 0;
        for (int i = 0; i < 32 && n != 0; ++i) {
            rev |= (n & 1) << (31 - i);
            n >>>= 1;
        }
        return rev;
    }

    // 方法一：（自己写的）逐位颠倒-时间复杂度：O(logn)，空间复杂度：O(1)
    public int reverseBits11(int n) {
        int res = 0;
        for (int i = 0; i < 31; i++) {
            if ((n & 1) == 1)
                res += 1;
            n = n >> 1;
            res = res << 1;
        }
        if ((n & 1) == 1)
            res += 1;
        return res;
    }

    // 方法二：位运算分治（类比 Integer.bitCount() 源码 类似归并）-时间复杂度：O(1)，空间复杂度：O(1)
    // 若要翻转一个二进制串，可以将其均分成左右两部分，对每部分递归执行翻转操作，然后将左半部分拼在右半部分的后面，即完成了翻转。
    // 由于左右两部分的计算方式是相似的，利用位掩码和位移运算，我们可以自底向上地完成这一分治流程。
    // 对于递归的最底层，我们需要交换所有奇偶位：
    // 取出所有奇数位和偶数位，将奇数位移到偶数位上，偶数位移到奇数位上。
    // 类似地，对于倒数第二层，每两位分一组，按组号取出所有奇数组和偶数组，然后将奇数组移到偶数组上，偶数组移到奇数组上。以此类推。
    private static final int M1 = 0x55555555; // 01010101010101010101010101010101
    private static final int M2 = 0x33333333; // 00110011001100110011001100110011
    private static final int M4 = 0x0f0f0f0f; // 00001111000011110000111100001111
    private static final int M8 = 0x00ff00ff; // 00000000111111110000000011111111

    public int reverseBits2(int n) {
        // 取出所有奇数位和偶数位，将奇数位移到偶数位上，偶数位移到奇数位上。
        n = ((n >>> 1) & M1) | ((n & M1) << 1);// 底层
        n = ((n >>> 2) & M2) | ((n & M2) << 2);
        n = ((n >>> 4) & M4) | ((n & M4) << 4);
        n = ((n >>> 8) & M8) | ((n & M8) << 8);// 顶层
        return n >>> 16 | n << 16;
    }

    // 191.位1的个数
    // 编写一个函数，输入是一个无符号整数（以二进制串的形式），返回其二进制表达式中数字位数为 '1' 的个数（也被称为汉明重量）。
    // 提示：
    // 请注意，在某些语言（如 Java）中，没有无符号整数类型。
    // 在这种情况下，输入和输出都将被指定为有符号整数类型，并且不应影响您的实现，
    // 因为无论整数是有符号的还是无符号的，其内部的二进制表示形式都是相同的。
    // 在 Java 中，编译器使用二进制补码记法来表示有符号整数。因此，在 示例 3 中，输入表示有符号整数 -3。
    // 提示：
    // 输入必须是长度为 32 的 二进制串 。
    // 进阶：
    // 如果多次调用这个函数，你将如何优化你的算法？

    // 方法一：循环检查二进制位-时间复杂度：O(k)，其中 k 是 int 型的二进制位数，k=32。
    // 我们需要检查 n 的二进制位的每一位，一共需要检查 32 位。
    // 空间复杂度：O(1)
    public int hammingWeight(int n) {
        int ret = 0;
        for (int i = 0; i < 32; i++)
            if ((n & (1 << i)) != 0)
                ret++;

        return ret;
    }

    // 方法二：位运算优化-时间复杂度：O(logn)，logn<=k，空间复杂度：O(1)
    // Brian Kernighan 算法
    // 原理是：对于任意整数 x，令 x = x & (x − 1)，该运算将 x 的二进制表示的最后一个 1 变成 0
    public int hammingWeight2(int n) {
        int ret = 0;
        while (n != 0) {
            n &= n - 1;
            ret++;
        }
        return ret;
    }

    // Integer.bitCount() 源码 类似归并
    public int hammingWeight3(int n) {
        n = n - ((n >>> 1) & 0x55555555);// 10101010101010101010101010101011 周期是2，每个周期是01
        n = (n & 0x33333333) + ((n >>> 2) & 0x33333333);// 11001100110011001100110011001101 周期是4，每个周期是0011
        n = (n + (n >>> 4)) & 0x0f0f0f0f;// 11110000111100001111000011110001 周期是8，每个是00001111
        n = n + (n >>> 8);
        n = n + (n >>> 16);
        return n & 0x3f;// 0011 1111
    }

    // 行一将i两两分组，每组为这两位中二进制1的个数
    // 按照原理，这行代码也可改写为(i & 0x55555555) + ((i >>> 1) & 0x55555555)
    // 行二将i四四分组，每组为这四位中二进制1的个数
    // 行三将i八八分组，每组为这八位中二进制1的个数
    // 行四将i按十六位分组，每组为这十六位中二进制1的个数，这里也分为了4组，其中2组为垃圾数据
    // 行四将i按三十二位分组，每组为这三十二位中二进制1的个数，这里也分为了4组，其中3组为垃圾数据
    // 行五取出行四运算结果中的有效数据，6位二进制足够表示最大的32位int中的bitCount
    public int hammingWeight33(int n) {
        n = (n & 0x55555555) + ((n >> 1) & 0x55555555);// 0101 0101 0101 0101 ... 周期是2，每个周期是 01
        n = (n & 0x33333333) + ((n >> 2) & 0x33333333);// 0011 0011 0011 0011 ... 周期是4，每个周期是 0011
        n = (n & 0x0f0f0f0f) + ((n >> 4) & 0x0f0f0f0f);// 0000 1111 0000 1111 ... 周期是8，每个是 0000 1111
        n = (n & 0x00ff00ff) + ((n >> 8) & 0x00ff00ff);// 0000 0000 1111 1111 ...
        n = (n & 0x0000ffff) + ((n >> 16) & 0x0000ffff);
        return n;
    }

    // 198.打家劫舍
    // 你是一个专业的小偷，计划偷窃沿街的房屋。
    // 每间房内都藏有一定的现金，影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，
    // 如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。
    // 给定一个代表每个房屋存放金额的非负整数数组，计算你 不触动警报装置的情况下 ，一夜之内能够偷窃到的最高金额。

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(1)
    // 如果房屋数量大于两间，应该如何计算能够偷窃到的最高总金额呢？对于第 k (k>2) 间房屋，有两个选项：
    // 偷窃第 k 间房屋，那么就不能偷窃第 k-1 间房屋，偷窃总金额为前 k-2 间房屋的最高总金额与第 k 间房屋的金额之和。
    // 不偷窃第 k 间房屋，偷窃总金额为前 k-1 间房屋的最高总金额。
    // 在两个选项中选择偷窃总金额较大的选项，该选项对应的偷窃总金额即为前 k 间房屋能偷窃到的最高总金额。
    // dp[i] 表示前 i 间房屋能偷窃到的最高总金额，那么就有如下的状态转移方程：
    // dp[i]=max(dp[i−2]+nums[i],dp[i−1])
    public int rob(int[] nums) {
        int length = nums.length;

        if (length == 1)
            return nums[0];

        // 考虑到每间房屋的最高总金额只和该房屋的前两间房屋的最高总金额相关，因此可以使用滚动数组，在每个时刻只需要存储前两间房屋的最高总金额。
        int first = nums[0], second = Math.max(nums[0], nums[1]);
        for (int i = 2; i < length; i++) {
            int temp = second;
            second = Math.max(first + nums[i], second);
            first = temp;
        }
        return second;
    }

    // 方法二：动态规划（自己写的）-时间复杂度：O(n)，空间复杂度：O(1)
    public int rob2(int[] nums) {
        int dp0 = 0;// 不偷当前的最高总额
        int dp1 = nums[0];// 偷当前的最高总额
        for (int i = 1; i < nums.length; i++) {
            int num = nums[i];
            int newdp0 = Math.max(dp0, dp1);
            int newdp1 = dp0 + num;
            dp0 = newdp0;
            dp1 = newdp1;
        }
        return Math.max(dp0, dp1);
    }

    // 200.岛屿数量
    // 给你一个由 '1'（陆地）和 '0'（水）组成的的二维网格，请你计算网格中岛屿的数量。
    // 岛屿总是被水包围，并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
    // 此外，你可以假设该网格的四条边均被水包围。
    // 提示：
    // m == grid.length
    // n == grid[i].length
    // 1 <= m, n <= 300
    // grid[i][j] 的值为 '0' 或 '1'

    // 方法一：dfs-时间复杂度、空间复杂度-O(mn)
    // 类似于图的dfs遍历，使用visited数组记录被访问过的节点，循环遍历每个未被访问过的节点，直到所有连通分支被访问
    // 该方法的优化点：只有1会访问，被访问过的点置为0，则之后的循环不会再访问该点，省去使用visited数组的空间
    void dfs(char[][] grid, int r, int c) {
        int m = grid.length;
        int n = grid[0].length;

        if (r < 0 || c < 0 || r >= m || c >= n || grid[r][c] == '0') // 为边界外，或为0
            return;

        grid[r][c] = '0';
        dfs(grid, r - 1, c);// 上
        dfs(grid, r + 1, c);// 下
        dfs(grid, r, c - 1);// 左
        dfs(grid, r, c + 1);// 右
    }

    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0)
            return 0;

        int m = grid.length;
        int n = grid[0].length;
        int num_islands = 0;
        for (int r = 0; r < m; ++r)
            for (int c = 0; c < n; ++c)
                if (grid[r][c] == '1') {
                    ++num_islands;
                    dfs(grid, r, c);
                }

        return num_islands;
    }

    // 方法二：bfs-时间复杂度：O(mn)，空间复杂度-O(min(m, n))
    // 同样省去使用visited数组
    // 使用bfs时一定要入队时就标记已访问
    public int numIslands2(char[][] grid) {
        if (grid == null || grid.length == 0)
            return 0;

        int m = grid.length;
        int n = grid[0].length;
        int num_islands = 0;

        for (int r = 0; r < m; ++r)// 上下边界
            for (int c = 0; c < n; ++c)// 左右边界
                if (grid[r][c] == '1') {
                    ++num_islands;
                    grid[r][c] = '0';
                    Queue<Integer> neighbors = new LinkedList<>();// 每个“连通分量”都使用一个辅助队列进行bfs
                    neighbors.add(r * n + c);// 将二维坐标转换成一维，入队
                    while (!neighbors.isEmpty()) {
                        int id = neighbors.remove();
                        int row = id / n;// 一维坐标转化为二维
                        int col = id % n;
                        if (row - 1 >= 0 && grid[row - 1][col] == '1') {// 其上为1则加入队列
                            neighbors.add((row - 1) * n + col);
                            grid[row - 1][col] = '0';
                        }
                        if (row + 1 < m && grid[row + 1][col] == '1') {// 下
                            neighbors.add((row + 1) * n + col);
                            grid[row + 1][col] = '0';
                        }
                        if (col - 1 >= 0 && grid[row][col - 1] == '1') {// 左
                            neighbors.add(row * n + col - 1);
                            grid[row][col - 1] = '0';
                        }
                        if (col + 1 < n && grid[row][col + 1] == '1') {// 右
                            neighbors.add(row * n + col + 1);
                            grid[row][col + 1] = '0';
                        }
                    }
                }

        return num_islands;
    }

    // 方法二：自己写的bfs-时间复杂度：O(mn)，空间复杂度-O(min(m, n))
    public int numIslands22(char[][] grid) {
        int m = grid.length;
        int n = grid[0].length;
        int res = 0;
        Queue<int[]> queue = new LinkedList<>();
        int directions[][] = new int[][] { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } };
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == '0')
                    continue;

                res++;
                queue.offer(new int[] { i, j });
                grid[i][j] = '0';
                while (!queue.isEmpty()) {
                    int[] node = queue.poll();
                    int node_i = node[0];
                    int node_j = node[1];
                    for (int[] direction : directions) {
                        int new_i = node_i + direction[0];
                        int new_j = node_j + direction[1];
                        if (0 <= new_i && new_i < m && 0 <= new_j && new_j < n && grid[new_i][new_j] == '1') {
                            queue.offer(new int[] { new_i, new_j });
                            grid[new_i][new_j] = '0';
                        }
                    }
                }
            }
        }
        return res;
    }

    // 方法三：并查集-时间复杂度：O(mn*α(mn))，空间复杂度：O(mn)
    // 当使用路径压缩（见 find 函数）和按秩合并（见数组 rank）实现并查集时，单次操作的时间复杂度为 α(mn)，
    // 其中 α(x) 为反阿克曼函数，当自变量 x 的值在人类可观测的范围内（宇宙中粒子的数量）时，函数 α(x) 的值不会超过 5，
    // 因此也可以看成是常数时间复杂度
    // 使用并查集代替搜索：
    // 为了求出岛屿的数量，我们可以扫描整个二维网格。如果一个位置为 1，则将其与相邻四个方向上的 1 在并查集中进行合并。
    // 最终岛屿的数量就是并查集中连通分量的数目。
    class UnionFind {// 内部类
        int count;// 连通分量的数目
        int[] parent;// 每个连通分量中的所有节点的最终parent，都为同一个（该连通分量的第一个被访问的节点）
        int[] rank;// 根据rank决定合并时x, y哪个节点作为“父节点”

        public UnionFind(char[][] grid) {// 构造函数初始化
            count = 0;
            int m = grid.length;
            int n = grid[0].length;
            parent = new int[m * n];
            rank = new int[m * n];
            for (int i = 0; i < m; ++i)
                for (int j = 0; j < n; ++j) {// 二维坐标转一维
                    if (grid[i][j] == '1') {
                        parent[i * n + j] = i * n + j;
                        ++count;// 只要为1就++，开始合并后--
                    }
                    rank[i * n + j] = 0;
                }

        }

        public int find(int i) {// 路径压缩，寻找节点i的“父节点”
            if (parent[i] != i)
                parent[i] = find(parent[i]);// 修改父节点指向，指向父节点的父节点
            return parent[i];
        }

        public void union(int x, int y) {// 按秩合并， x, y为节点坐标，x 与 y 在并查集中进行合并
            int rootx = find(x);
            int rooty = find(y);
            if (rootx != rooty) {// root一样则两个点已经合并过了，不一样则合并，根据rank大小决定谁是“父节点”
                if (rank[rootx] > rank[rooty])
                    parent[rooty] = rootx;
                else if (rank[rootx] < rank[rooty])
                    parent[rootx] = rooty;
                else {// x, y节点rank相等，x的rank++
                    parent[rooty] = rootx;
                    rank[rootx] += 1;
                }
                --count;
            }
        }

        public int getCount() {
            return count;
        }
    }

    public int numIslands3(char[][] grid) {
        if (grid == null || grid.length == 0)
            return 0;

        int m = grid.length;
        int n = grid[0].length;
        UnionFind uf = new UnionFind(grid);
        for (int r = 0; r < m; ++r) // 上下边界
            for (int c = 0; c < n; ++c) // 左右边界
                if (grid[r][c] == '1') {// 如果一个位置为 1，则将其与相邻四个方向上的 1 在并查集中进行合并
                    grid[r][c] = '0';// 访问过的置为0
                    if (r - 1 >= 0 && grid[r - 1][c] == '1')// 上
                        uf.union(r * n + c, (r - 1) * n + c);

                    if (r + 1 < m && grid[r + 1][c] == '1')// 下
                        uf.union(r * n + c, (r + 1) * n + c);

                    if (c - 1 >= 0 && grid[r][c - 1] == '1')// 左
                        uf.union(r * n + c, r * n + c - 1);

                    if (c + 1 < n && grid[r][c + 1] == '1')// 右
                        uf.union(r * n + c, r * n + c + 1);

                }

        return uf.getCount();
    }

    // 202.快乐数
    // 编写一个算法来判断一个数 n 是不是快乐数。
    // 「快乐数」 定义为：
    // 对于一个正整数，每一次将该数替换为它每个位置上的数字的平方和。
    // 然后重复这个过程直到这个数变为 1，也可能是 无限循环 但始终变不到 1。
    // 如果这个过程 结果为 1，那么这个数就是快乐数。
    // 如果 n 是 快乐数 就返回 true ；不是，则返回 false 。
    // 提示：
    // 1 <= n <= 231 - 1

    // 方法一：用哈希集合检测循环-时间复杂度：O(logn)，空间复杂度：O(logn)
    class ssoltion_202_1 {
        private int getNext(int n) {
            int totalSum = 0;
            while (n > 0) {
                int d = n % 10;
                n = n / 10;
                totalSum += d * d;
            }
            return totalSum;
        }

        public boolean isHappy(int n) {
            Set<Integer> seen = new HashSet<>();
            while (n != 1 && !seen.contains(n)) {
                seen.add(n);
                n = getNext(n);
            }
            return n == 1;
        }
    }

    // 方法二：快慢指针法-时间复杂度：O(logn)，空间复杂度：O(1)
    class ssolution_202_2 {
        public int getNext(int n) {
            int totalSum = 0;
            while (n > 0) {
                int d = n % 10;
                n = n / 10;
                totalSum += d * d;
            }
            return totalSum;
        }

        public boolean isHappy(int n) {
            int slowRunner = n;
            int fastRunner = getNext(n);
            while (fastRunner != 1 && slowRunner != fastRunner) {
                slowRunner = getNext(slowRunner);
                fastRunner = getNext(getNext(fastRunner));
            }
            return fastRunner == 1;
        }
    }

    // 方法三：数学-时间复杂度：O(logn)，空间复杂度：O(1)
    // Digits Largest Next
    // 1 9 81
    // 2 99 162
    // 3 999 243
    // 4 9999 324
    // 13 9999999999999 1053
    // 下一个值可能比自己大的最大数字是什么？
    // 根据我们之前的分析，我们知道它必须低于 243。
    // 因此，我们知道任何循环都必须包含小于 243 的数字，用这么小的数字，编写一个能找到所有周期的强力程序并不困难。
    // 如果这样做，您会发现只有一个循环：4 → 16 → 37 → 58 → 89 → 145 → 42 → 20 → 4。
    // 所有其他数字都在进入这个循环的链上，或者在进入 1 的链上。
    // 因此，我们可以硬编码一个包含这些数字的散列集，如果我们达到其中一个数字，那么我们就知道在循环中。
    class ssolution_202_3 {
        private static Set<Integer> cycleMembers = new HashSet<>(Arrays.asList(4, 16, 37, 58, 89, 145, 42, 20));

        public int getNext(int n) {
            int totalSum = 0;
            while (n > 0) {
                int d = n % 10;
                n = n / 10;
                totalSum += d * d;
            }
            return totalSum;
        }

        public boolean isHappy(int n) {
            while (n != 1 && !cycleMembers.contains(n))
                n = getNext(n);
            return n == 1;
        }
    }

    // 204.计数质数
    // 给定整数 n ，返回 所有小于非负整数 n 的质数的数量 。
    // 提示：
    // 0 <= n <= 5 * 106

    // 方法一：枚举-时间复杂度：O(n·n^-1/2)，空间复杂度：O(1)
    class ssolution_204_1 {
        public int countPrimes(int n) {
            int ans = 0;
            for (int i = 2; i < n; ++i)
                ans += isPrime(i) ? 1 : 0;
            return ans;
        }

        public boolean isPrime(int x) {
            for (int i = 2; i * i <= x; ++i)
                if (x % i == 0)
                    return false;
            return true;
        }
    }

    // 方法二：埃氏筛（标记合数）-时间复杂度：O(nloglogn)，空间复杂度：O(n)，具体证明这里不再展开
    // 我们考虑这样一个事实：如果 x 是质数，那么大于 x 的 x 的倍数 2x,3x,… 一定不是质数，因此我们可以从这里入手。
    public int countPrimes2(int n) {
        int[] isPrime = new int[n];
        Arrays.fill(isPrime, 1);
        int ans = 0;
        for (int i = 2; i < n; ++i) {
            if (isPrime[i] == 1) {// i是质数
                ans += 1;
                if ((long) i * i < n)// 标记合数
                    for (int j = i * i; j < n; j += i)
                        isPrime[j] = 0;
            }
        }
        return ans;
    }

    // 方法三：线性筛-时间复杂度：O(n)，空间复杂度：O(n)
    // 实际执行时间甚至更长...
    // 埃氏筛其实还是存在冗余的标记操作，比如对于 45 这个数，它会同时被 3,5 两个数标记为合数，
    // 因此我们优化的目标是让每个合数只被标记一次，这样时间复杂度即能保证为 O(n)，这就是我们接下来要介绍的线性筛。
    // 相较于埃氏筛，我们多维护一个 primes 数组表示当前得到的质数集合。我们从小到大遍历，如果当前的数 x 是质数，就将其加入 primes 数组。
    // 另一点与埃氏筛不同的是，「标记过程」不再仅当 x 为质数时才进行，而是对每个整数 x 都进行。
    // 对于整数 x，我们不再标记其所有的倍数 x⋅2,x⋅3,…，而是只标记质数集合中的数与 x 相乘的数，
    // 即 x⋅primes0,x⋅primes1,…，且在发现 x mod primesi = 0 的时候结束当前标记。
    public int countPrimes3(int n) {
        List<Integer> primes = new ArrayList<Integer>();
        int[] isPrime = new int[n];
        Arrays.fill(isPrime, 1);
        for (int i = 2; i < n; ++i) {
            if (isPrime[i] == 1)
                primes.add(i);

            for (int j = 0; j < primes.size() && i * primes.get(j) < n; ++j) {
                isPrime[i * primes.get(j)] = 0;
                if (i % primes.get(j) == 0)// 防止重复标记合数
                    break;
            }
        }
        return primes.size();
    }

    // 206.反转链表
    // 给你单链表的头节点 head ，请你反转链表，并返回反转后的链表。
    // 提示：
    // 链表中节点的数目范围是 [0, 5000]
    // -5000 <= Node.val <= 5000
    // 进阶：链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题？

    // 方法一：迭代-时间复杂度：O(n)，空间复杂度：O(1)
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {// 由curr跳出，需要避免next空指针
            ListNode next = curr.next;// 可看做临时节点，存放curr.next
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }

    // 方法二：递归-时间复杂度：O(n)，空间复杂度：O(n)
    public ListNode reverseList2(ListNode head) {
        if (head == null || head.next == null)// 尾节点开始返回
            return head;
        // ListNode nextNode = head.next; 减少额外的空间开销
        ListNode newHead = reverseList2(head.next);
        head.next.next = head;// 下一个节点的next指向当前节点

        // 当前节点指向null 后续会被更改，直到重新回到原头节点，防止新链表末尾（原链表头）成环
        head.next = null;
        return newHead;
    }

    // 207.课程表
    // 你这个学期必须选修 numCourses 门课程，记为 0 到 numCourses - 1 。
    // 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出，其中 prerequisites[i] = [ai, bi] ，
    // 表示如果要学习课程 ai 则 必须 先学习课程  bi 。
    // 例如，先修课程对 [0, 1] 表示：想要学习课程 0 ，你需要先完成课程 1 。
    // 请你判断是否可能完成所有课程的学习？如果可以，返回 true ；否则，返回 false 。

    // 你这个学期必须选修 numCourses 门课程，记为 0 到 numCourses - 1 。
    // 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出，其中 prerequisites[i] = [ai, bi] ，
    // 表示如果要学习课程 ai 则 必须 先学习课程  bi 。
    // 例如，先修课程对 [0, 1] 表示：想要学习课程 0 ，你需要先完成课程 1 。
    // 请你判断是否可能完成所有课程的学习？如果可以，返回 true ；否则，返回 false 。
    // 提示：
    // 1 <= numCourses <= 1e5
    // 0 <= prerequisites.length <= 5000
    // prerequisites[i].length == 2
    // 0 <= ai, bi < numCourses
    // prerequisites[i] 中的所有课程对 互不相同
    // 「判断图 G 是否存在拓扑排序」，至少也要对其进行一次完整的遍历，时间复杂度也为 O(n+m)
    // 因此不可能存在一种仅判断图是否存在拓扑排序的方法。它的时间复杂度在渐进意义上严格优于 O(n+m)

    // 方法一：bfs（辅助队列）（判据：是否访问到了所有节点，有环必不能访问到所有节点）-时间复杂度、空间复杂度-O(n+m)
    // 我们使用一个队列来进行广度优先搜索。初始时，所有入度为 0 的节点都被放入队列中，
    // 它们就是可以作为拓扑排序最前面的节点，并且它们之间的相对顺序是无关紧要的。
    // 在广度优先搜索的每一步中，我们取出队首的节点 u：
    // 我们将 u 放入答案中；
    // 我们移除 u 的所有出边，也就是将 u 的所有相邻节点的入度减少 1。如果某个相邻节点 v 的入度变为 0，那么我们就将 v 放入队列中。
    // 在广度优先搜索的过程结束后。如果答案中包含了这 n 个节点，那么我们就找到了一种拓扑排序，否则说明图中存在环，也就不存在拓扑排序了。
    // 优化：
    // 由于我们只需要判断是否存在一种拓扑排序，因此我们省去存放答案数组，而是只用一个变量记录被放入答案数组的节点个数。
    // 在广度优先搜索结束之后，我们判断该变量的值是否等于课程数，就能知道是否存在一种拓扑排序。

    public boolean canFinish(int numCourses, int[][] prerequisites) {
        List<List<Integer>> edges2;// 存放后继节点
        int[] indeg;// 入度

        edges2 = new ArrayList<List<Integer>>();// 不定长顺序表
        for (int i = 0; i < numCourses; ++i)
            edges2.add(new ArrayList<Integer>());

        indeg = new int[numCourses];
        for (int[] info : prerequisites) {
            edges2.get(info[1]).add(info[0]);// 存放后继节点
            ++indeg[info[0]];// 后继节点入度+1
        }

        Queue<Integer> queue = new LinkedList<Integer>();// 辅助队列
        for (int i = 0; i < numCourses; ++i)
            if (indeg[i] == 0)// 入度为0的入队
                queue.offer(i);

        int visited = 0;
        while (!queue.isEmpty()) {
            ++visited;
            int u = queue.poll();// 出队
            for (int v : edges2.get(u)) {
                --indeg[v];// 后继节点入度-1
                if (indeg[v] == 0) // 入队为0则入队
                    queue.offer(v);
            }
        }

        return visited == numCourses;// 所有节点均被访问过，则无环，否则存在环
    }

    // 方法二：dfs（判据：根据visited数组判断是否有环）-时间复杂度、空间复杂度-O(n+m)
    // 对于图中的任意一个节点，它在搜索的过程中有三种状态，即：
    // 「未搜索」：我们还没有搜索到这个节点；
    // 「搜索中」：我们搜索过这个节点，但还没有回溯到该节点，即该节点还没有入栈，还有相邻的节点没有搜索完成；
    // 「已完成」：我们搜索过并且回溯过这个节点，即该节点已经入栈，并且所有该节点的相邻节点都出现在栈的更底部的位置，满足拓扑排序的要求。
    // 将当前搜索的节点 u 标记为「搜索中」，遍历该节点的每一个相邻节点 v：
    // 如果 v 为「未搜索」，那么我们开始搜索 v，待搜索完成回溯到 u；
    // 如果 v 为「搜索中」，那么我们就找到了图中的一个环，因此是不存在拓扑排序的；
    // 如果 v 为「已完成」，那么说明 v 已经在栈中了，而 u 还不在栈中，
    // 因此 u 无论何时入栈都不会影响到 (u, v)之前的拓扑关系，以及不用进行任何操作。
    // 优化：
    // 由于我们只需要判断是否存在一种拓扑排序，而「栈的作用仅仅是存放最终的拓扑排序结果」，因此我们可以只记录每个节点的状态，而省去对应的栈。

    class ssoltion_207_2 {
        List<List<Integer>> edges;// 存放后继节点
        int[] visited;// 标记每个节点的状态：0=未搜索，1=搜索中，2=已完成
        boolean valid = true;

        public boolean canFinish2(int numCourses, int[][] prerequisites) {
            edges = new ArrayList<List<Integer>>();// 不定长顺序表
            for (int i = 0; i < numCourses; ++i)
                edges.add(new ArrayList<Integer>());

            visited = new int[numCourses];
            for (int[] info : prerequisites)
                edges.get(info[1]).add(info[0]);// 存放后继节点

            for (int i = 0; i < numCourses && valid; ++i)// 每一次遍历的是有向图的一个连通分支
                if (visited[i] == 0)// 未搜索
                    dfs(i);

            return valid;
        }

        public void dfs(int u) {
            visited[u] = 1;// 标记为正在搜索的节点
            for (int v : edges.get(u)) {// 循环遍历该节点所有后继节点
                if (visited[v] == 0) {
                    dfs(v);
                    if (!valid)// 判断是否可提前退出递归
                        return;

                } else if (visited[v] == 1) {// 遇到正在搜索的节点了，即存在环
                    valid = false;
                    return;
                }
            }
            visited[u] = 2;// 标记为已完成
        }
    }

    // 208.实现Trie (前缀树)
    // Trie（发音类似 "try"）或者说 前缀树 是一种树形数据结构，用于高效地存储和检索字符串数据集中的键。
    // 这一数据结构有相当多的应用情景，例如自动补完和拼写检查。
    // 请你实现 Trie 类：
    // Trie() 初始化前缀树对象。
    // void insert(String word)
    // 向前缀树中插入字符串 word 。
    // boolean search(String word)
    // 如果字符串 word 在前缀树中，返回 true（即，在检索之前已经插入）；否则，返回 false。
    // boolean startsWith(String prefix)
    // 如果之前已经插入的字符串 word 的前缀之一为 prefix ，返回 true ；否则，返回 false。
    // 提示：
    // 1 <= word.length, prefix.length <= 2000
    // word 和 prefix 仅由小写英文字母组成
    // insert、search 和 startsWith 调用次数 总计 不超过 3 * 104 次

    // 方法一：字典树-时间复杂度：初始化为O(1)，其余操作为 O(|S|)，其中 |S| 是每次插入或查询的字符串的长度。
    // 空间复杂度：O(|T|⋅Σ)，其中 |T| 为所有插入字符串的长度之和，Σ 为字符集的大小，本题 Σ=26。
    // Trie，又称前缀树或字典树，是一棵有根树，其每个节点包含以下字段：
    // 指向子节点的指针数组 children。对于本题而言，数组长度为 26，即小写英文字母的数量。
    // 此时 children[0] 对应小写字母 a，children[1] 对应小写字母 b，…，children[25] 对应小写字母 z。
    // 布尔字段 isEnd，表示该节点是否为字符串的结尾。
    // （本题可看成一棵26叉树，以边表示字母）
    class Trie {
        TrieNode headNode;

        Trie() {
            headNode = new TrieNode();
        }

        private class TrieNode {
            TrieNode[] children;
            boolean isEnd; // 默认false

            TrieNode() {
                children = new TrieNode[26];
            }
        }

        public void insert(String word) {
            TrieNode node = headNode;// node指向头节点
            for (int i = 0; i < word.length(); i++) {// 逐个遍历每个字符
                char ch = word.charAt(i);
                int index = ch - 'a'; // 从a开始索引值为0
                if (node.children[index] == null) // 若无则创建节点
                    node.children[index] = new TrieNode();

                node = node.children[index];// 指向当前字符
            }
            node.isEnd = true;// 表示字符串结尾
        }

        public boolean search(String word) {
            TrieNode node = searchPrefix(word);
            return node != null && node.isEnd;// 存在该字符串，且为字符串结尾
        }

        public boolean startsWith(String prefix) {
            return searchPrefix(prefix) != null;// 无需为字符串结尾
        }

        // 查询前缀，返回末尾字符节点
        private TrieNode searchPrefix(String prefix) {
            TrieNode node = headNode;
            for (int i = 0; i < prefix.length(); i++) {
                char ch = prefix.charAt(i);
                int index = ch - 'a';
                if (node.children[index] == null)
                    return null;

                node = node.children[index];
            }
            return node;
        }
    }

    // 方法一：（自己写的）前缀树
    class Trie1 {
        Trie1[] children;
        boolean isEnd;

        public Trie1() {
            children = new Trie1[26];
        }

        public void insert(String word) {
            Trie1 trie = this;
            for (char curr : word.toCharArray()) {
                int index = curr - 'a';
                if (trie.children[index] == null)
                    trie.children[index] = new Trie1();

                trie = trie.children[index];
            }
            trie.isEnd = true;
        }

        public boolean search(String word) {
            Trie1 trie = this;
            for (char curr : word.toCharArray()) {
                int index = curr - 'a';
                if (trie.children[index] == null)
                    return false;
                trie = trie.children[index];
            }
            return trie.isEnd;
        }

        public boolean startsWith(String prefix) {
            Trie1 trie = this;
            for (char curr : prefix.toCharArray()) {
                int index = curr - 'a';
                if (trie.children[index] == null)
                    return false;
                trie = trie.children[index];
            }
            return true;
        }
    }

    // 方法二：有序集合-时间复杂度：初始化为O(1)，其余操作为O(log|S|)
    // 空间复杂度：O(n)，字符串的个数
    // （自己的lowb想法：二叉排序树（java的TreeSet），查找前缀：先找插入位置，再比较父节点）
    // 在时间复杂度上，前缀树解法明显优于有序集合解法。
    // 其原因在于：使用前缀树（Trie）的数据结构使得插入、查询全词、查询前缀的时间复杂度与已插入的单词数目无关，
    // 这是前缀树（Trie）解法优于有序集合解法的关键
    class Trie2 {
        private TreeSet<String> treeSet;

        Trie2() {
            treeSet = new TreeSet<>();
        }

        public void insert(String word) {
            treeSet.add(word);
        }

        public boolean search(String word) {
            return treeSet.contains(word);
        }

        public boolean startsWith(String prefix) {
            if (treeSet.contains(prefix))
                return true;

            // 前缀可能包含prefix的字符串一定大于prefix（字符串的比较）
            String high = treeSet.higher(prefix);
            if (high != null && high.startsWith(prefix))
                return true;
            return false;
        }
    }

    // 210.课程表 II
    // 现在你总共有 numCourses 门课需要选，记为 0 到 numCourses - 1。
    // 给你一个数组 prerequisites ，其中 prerequisites[i] = [ai, bi] ，表示在选修课程 ai 前 必须 先选修 bi。
    // 例如，想要学习课程 0 ，你需要先完成课程 1 ，我们用一个匹配来表示：[0,1] 。
    // 返回你为了学完所有课程所安排的学习顺序。
    // 可能会有多个正确的顺序，你只要返回 任意一种 就可以了。如果不可能完成所有课程，返回 一个空数组 。
    // 提示：
    // 1 <= numCourses <= 2000
    // 0 <= prerequisites.length <= numCourses * (numCourses - 1)
    // prerequisites[i].length == 2
    // 0 <= ai, bi < numCourses
    // ai != bi
    // 所有[ai, bi] 互不相同

    // 方法一：深度优先搜索 dfs-时间复杂度：O(n+m)，空间复杂度：O(n+m)
    class ssolution_210_1 {
        List<List<Integer>> edges;// 存储有向图
        int[] visited;// 标记每个节点的状态：0=未搜索，1=搜索中，2=已完成
        int[] result;// 用数组来模拟栈，下标 n-1 为栈底，0 为栈顶
        boolean valid = true;// 判断有向图中是否有环
        int index;// 栈下标

        public int[] findOrder(int numCourses, int[][] prerequisites) {
            edges = new ArrayList<List<Integer>>();
            for (int i = 0; i < numCourses; ++i)
                edges.add(new ArrayList<Integer>());

            visited = new int[numCourses];
            result = new int[numCourses];
            index = numCourses - 1;
            for (int[] info : prerequisites)
                edges.get(info[1]).add(info[0]);

            // 每次挑选一个「未搜索」的节点，开始进行深度优先搜索
            for (int i = 0; i < numCourses && valid; ++i)
                if (visited[i] == 0)
                    dfs(i);

            if (!valid)
                return new int[0];

            return result;// 如果没有环，那么就有拓扑排序
        }

        public void dfs(int u) {
            visited[u] = 1;// 将节点标记为「搜索中」
            // 搜索其相邻节点
            // 只要发现有环，立刻停止搜索
            for (int v : edges.get(u)) {
                if (visited[v] == 0) {// 如果「未搜索」那么搜索相邻节点
                    dfs(v);
                    if (!valid)
                        return;
                } else if (visited[v] == 1) {// 如果「搜索中」说明找到了环
                    valid = false;
                    return;
                }
            }
            visited[u] = 2;// 将节点标记为「已完成」
            result[index--] = u;// 将节点入栈
        }
    }

    // 方法二：广度优先搜索 bfs-时间复杂度：O(n+m)，空间复杂度：O(n+m)
    class ssolution_210_2 {
        List<List<Integer>> edges;// 存储有向图
        int[] indeg;// 存储每个节点的入度
        int[] result;// 存储答案
        int index;// 答案下标

        public int[] findOrder(int numCourses, int[][] prerequisites) {
            edges = new ArrayList<List<Integer>>();
            for (int i = 0; i < numCourses; ++i)
                edges.add(new ArrayList<Integer>());

            indeg = new int[numCourses];
            result = new int[numCourses];
            index = 0;
            for (int[] info : prerequisites) {
                edges.get(info[1]).add(info[0]);
                ++indeg[info[0]];
            }

            Queue<Integer> queue = new LinkedList<Integer>();
            // 将所有入度为 0 的节点放入队列中
            for (int i = 0; i < numCourses; ++i)
                if (indeg[i] == 0)
                    queue.offer(i);

            while (!queue.isEmpty()) {

                int u = queue.poll();// 从队首取出一个节点

                result[index++] = u;// 放入答案中
                for (int v : edges.get(u)) {
                    --indeg[v];
                    // 如果相邻节点 v 的入度为 0，就可以选 v 对应的课程了
                    if (indeg[v] == 0)
                        queue.offer(v);
                }
            }

            if (index != numCourses)
                return new int[0];
            return result;
        }
    }

    // 212.单词搜索II
    // 给定一个 m x n 二维字符网格 board 和一个单词（字符串）列表 words， 返回所有二维网格上的单词 。
    // 单词必须按照字母顺序，通过 相邻的单元格 内的字母构成，其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。
    // 同一个单元格内的字母在一个单词中不允许被重复使用。
    // 提示：
    // m == board.length
    // n == board[i].length
    // 1 <= m, n <= 12
    // board[i][j] 是一个小写英文字母
    // 1 <= words.length <= 3 * 104
    // 1 <= words[i].length <= 10
    // words[i] 由小写英文字母组成
    // words 中的所有字符串互不相同

    // 方法一：回溯 + 字典树-空间复杂度：O(k×l)，其中 k 是 words 的长度，l 是最长单词的长度。
    class ssoltion_212_1 {
        public List<String> findWords(char[][] board, String[] words) {
            Trie trie = new Trie();
            for (String word : words)
                trie.insert(word);

            Set<String> ans = new HashSet<String>();// 可能存在多条符合某字符串的路径，因此需要选用Set避免放入重复结果
            for (int i = 0; i < board.length; ++i)
                for (int j = 0; j < board[0].length; ++j)
                    dfs(board, trie, i, j, ans);

            return new ArrayList<String>(ans);
        }

        public void dfs(char[][] board, Trie now, int i1, int j1, Set<String> ans) {
            char ch = board[i1][j1];
            if (!now.children.containsKey(ch))
                return;

            now = now.children.get(ch);
            if (!"".equals(now.word))
                ans.add(now.word);

            int[][] dirs = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } };
            board[i1][j1] = '#';// 原地修改，表示已经遍历过
            for (int[] dir : dirs) {
                int i2 = i1 + dir[0], j2 = j1 + dir[1];
                if (i2 >= 0 && i2 < board.length && j2 >= 0 && j2 < board[0].length)
                    dfs(board, now, i2, j2, ans);
            }
            board[i1][j1] = ch;// 还原
        }

        class Trie {
            Map<Character, Trie> children;// 字典树的分支选用Map结构，方便dfs时快速判断是否存在匹配字符
            String word = "";// 默认值为""，比使用String默认值null判断更快，为什么呢

            public Trie() {
                this.children = new HashMap<Character, Trie>();
            }

            public void insert(String word) {
                Trie cur = this;
                for (int i = 0; i < word.length(); ++i) {
                    char c = word.charAt(i);
                    if (!cur.children.containsKey(c))
                        cur.children.put(c, new Trie());
                    cur = cur.children.get(c);
                }
                cur.word = word;
            }
        }
    }

    // 方法一：（优化）删除被匹配的单词
    // 遍历时修改字典树，将匹配到的单词从字典树中移除，来避免重复寻找相同的单词。
    class ssoltion_212_11 {
        public List<String> findWords(char[][] board, String[] words) {
            Trie trie = new Trie();
            for (String word : words)
                trie.insert(word);

            List<String> ans = new ArrayList<String>();// 直接选用List存放结果，不用担心重复结果
            for (int i = 0; i < board.length; ++i)
                for (int j = 0; j < board[0].length; ++j)
                    dfs(board, trie, i, j, ans);

            return ans;
        }

        public void dfs(char[][] board, Trie now, int i1, int j1, List<String> ans) {
            char ch = board[i1][j1];
            if (!now.children.containsKey(ch))
                return;

            Trie nxt = now.children.get(ch);
            if (!"".equals(nxt.word)) {
                ans.add(nxt.word);
                nxt.word = "";// 删除被匹配的单词
            }

            // 下一层没有分支，则可以去掉当前层Map中的对应键值对，并提前剪枝
            if (nxt.children.isEmpty()) {
                now.children.remove(ch);
                return;
            }

            int[][] dirs = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } };
            board[i1][j1] = '#';// 原地修改，表示已经遍历过
            for (int[] dir : dirs) {
                int i2 = i1 + dir[0], j2 = j1 + dir[1];
                if (i2 >= 0 && i2 < board.length && j2 >= 0 && j2 < board[0].length)
                    dfs(board, nxt, i2, j2, ans);
            }
            board[i1][j1] = ch;// 还原
        }

        class Trie {
            Map<Character, Trie> children;// 字典树的分支选用Map结构，方便dfs时快速判断是否存在匹配字符
            String word = "";// // 默认值为""，比使用String默认值null判断更快，为什么呢

            public Trie() {
                this.children = new HashMap<Character, Trie>();
            }

            public void insert(String word) {
                Trie cur = this;
                for (int i = 0; i < word.length(); ++i) {
                    char c = word.charAt(i);
                    if (!cur.children.containsKey(c))
                        cur.children.put(c, new Trie());
                    cur = cur.children.get(c);
                }
                cur.word = word;
            }
        }
    }

    // 215.数组中的第K个最大元素
    // 给定整数数组 nums 和整数 k，请返回数组中第 k 个最大的元素。
    // 请注意，你需要找的是数组排序后的第 k 个最大的元素，而不是第 k 个不同的元素。
    // 提示：
    // 1 <= k <= nums.length <= 104
    // -104 <= nums[i] <= 104

    // 方法一：基于快速排序的选择方法-时间复杂度：O(n)，空间复杂度：O(logn)
    // 可以用快速排序来解决这个问题，先对原数组排序，再返回倒数第 k 个位置，这样平均时间复杂度是 O(nlogn)，但其实我们可以做的更快。
    // 改进快速排序算法来解决这个问题：
    // 在分解的过程当中，我们会对子数组进行划分，如果划分得到的 q 正好就是我们需要的下标，就直接返回 a[q]；
    // 否则，如果 q 比目标下标小，就递归右子区间，否则递归左子区间。
    // 这样就可以把原来递归两个区间变成只递归一个区间，提高了时间效率。这就是「快速选择」算法。

    // 引入随机化来加速这个过程，它的时间代价的期望是 O(n)
    public int findKthLargest(int[] nums, int k) {
        quickSort(nums, k - 1, 0, nums.length - 1);// 第k大转换为相应下标
        return nums[k - 1];
    }

    private void quickSort(int[] nums, int index, int left, int right) {
        int pivotIndex = partition(nums, left, right);
        if (pivotIndex == index) // pivot最终位置的下标就是要找的
            return;

        if (pivotIndex < index) // 根据结果只选择一边继续查找
            quickSort(nums, index, pivotIndex + 1, right);
        else
            quickSort(nums, index, left, pivotIndex - 1);

    }

    private int partition(int[] nums, int left, int right) {
        Random random = new Random();
        int pivotIndex = random.nextInt(right - left + 1) + left;// 在区间left right之间随机选择一个pivot
        int pivot = nums[pivotIndex];
        swap(nums, pivotIndex, right);// pivot放到最右边去当哨兵
        int index = left;// 交换位
        for (int i = left; i < right; i++) // 索引right上是pivot
            if (nums[i] > pivot) {// 大于pivot的数放在交换位上，即大于pivot的数在左边，降序排列，同理升序（小的放左边）
                swap(nums, i, index);
                index++;// 交换位右移一位
            }

        swap(nums, index, right);// pivot放到最终位置上
        return index;

    }

    // private void swap(int[] nums, int index1, int index2) {
    // int temp = nums[index1];
    // nums[index1] = nums[index2];
    // nums[index2] = temp;
    // }

    // 方法二：基于堆排序的选择方法（不借助优先队列API）-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 索引的处理像个猪比，看（自己写的）
    // 也可以使用堆排序来解决这个问题——建立一个大根堆（数组映射到树），做 k - 1 次删除操作后堆顶元素就是我们要找的答案。
    int[] myNums;

    public int findKthLargest2(int[] nums, int k) {
        myNums = nums;
        int heapSize = nums.length;
        buildMaxHeap(heapSize);// 建立大根堆
        for (int i = nums.length - 1; i >= nums.length - k + 1; --i) {// i始终指向末尾叶子节点
            swap(0, i);// 交换最大值与末尾叶子节点
            --heapSize;// 等价于删除最大值
            maxHeapify(0, heapSize);// 在指定范围内，将以i为根的子树调整为大根堆
        }
        return nums[0];
    }

    // 建立大根堆
    public void buildMaxHeap(int heapSize) {
        // 对于所有分支节点（节点数向下取整，非叶子节点），从后往前依次调整
        // 不能从前往后，要先处理底层，使可能沉底的最大元素逐层上浮直至最顶
        for (int i = heapSize / 2; i >= 0; --i)
            maxHeapify(i, heapSize);
    }

    // 在指定范围内，将以i为根的子树调整为大根堆（小元素下沉）
    public void maxHeapify(int root, int heapSize) {
        int l = root * 2 + 1, r = root * 2 + 2;// root节点的左右孩子
        int largest = root;
        if (l < heapSize && myNums[l] > myNums[largest])// 左孩子处于范围内且左孩子更大
            largest = l;

        if (r < heapSize && myNums[r] > myNums[largest])// 右孩子处于范围内且右孩子最大
            largest = r;

        if (largest != root) {// root不是最大的
            swap(root, largest);// 交换与最大节点
            maxHeapify(largest, heapSize);// 递归调整
        }
    }

    public void swap(int i, int j) {
        int temp = myNums[i];
        myNums[i] = myNums[j];
        myNums[j] = temp;
    }

    // 方法二：（自己写的）实现大根堆-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 不在原地建堆，而是复制到新的数组中建堆，下标1开始，方便计算完全二叉树的节点编号
    int[] heap;
    int size;

    // int n;
    public int findKthLargest22(int[] nums, int k) {
        n = nums.length;
        // 1 <= k <= nums.length <= 104 初值为0，不影响堆
        // 填充节点，简化最后一个非叶子节点的操作
        heap = new int[n + 2];
        for (int i = 0; i < n; i++) {
            heap[i + 1] = nums[i];
            size++;
        }

        for (int i = size / 2; i >= 1; i--) // 注意顺序！一定是非叶子节点，从后往前！
            buildHeap(i);

        for (int i = 1; i <= k - 1; i++) {// 出堆k-1个元素，此时堆顶即第k大的元素
            // 最后一个节点放到堆顶，并做下沉操作
            heap[1] = heap[size];
            size--;
            buildHeap(1);
        }
        return heap[1];
    }

    private void buildHeap(int rootIndex) {
        if (rootIndex > size / 2)// 不操作叶子节点
            return;
        int leftIndex = rootIndex * 2;
        int rightIndex = rootIndex * 2 + 1;

        int root = heap[rootIndex];
        int left = heap[leftIndex];
        int right = heap[rightIndex];

        if (root >= left && root >= right)// 已满足堆的性质，无需操作
            return;

        if (left >= right) {
            swap(leftIndex, rootIndex);
            buildHeap(leftIndex);
        } else {
            swap(rightIndex, rootIndex);
            buildHeap(rightIndex);
        }
    }

    // private void swap(int i, int j){
    // int temp = heap[i];
    // heap[i] = heap[j];
    // heap[j] = temp;
    // }

    // 方法三：利用javaAPI的做快排Arrays.sort()，堆排PriorityQueue
    // -时间复杂度：O(nlogk)，空间复杂度：O(k)
    public int findKthLargest3(int[] nums, int k) {
        // 长度为k的小根堆
        PriorityQueue<Integer> pq = new PriorityQueue<>(k, (num1, num2) -> num1 - num2);
        for (int num : nums)
            if (pq.size() < k)
                pq.offer(num);
            else {// 优先队列长度为k，更新最小值
                if (num > pq.peek()) {
                    pq.poll();
                    pq.offer(num);
                }
            }

        return pq.peek();
    }

    // 217.存在重复元素
    // 给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ，返回 true ；如果数组中每个元素互不相同，返回 false 。
    // 提示：
    // 1 <= nums.length <= 105
    // -109 <= nums[i] <= 109

    // 方法一：排序-时间复杂度：O(nlogn)，空间复杂度：O(logn)
    // 在对数字从小到大排序之后，数组的重复元素一定出现在相邻位置中。
    // 因此，我们可以扫描已排序的数组，每次判断相邻的两个元素是否相等，如果相等则说明存在重复的元素。

    // 方法二：哈希表-时间复杂度：O(n)，空间复杂度：O(n)
    public boolean containsDuplicate(int[] nums) {
        Set<Integer> set = new HashSet<Integer>();
        for (int x : nums) {
            if (!set.add(x))
                return true;
        }
        return false;
    }

    // 218.天际线问题
    // 城市的 天际线 是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。
    // 给你所有建筑物的位置和高度，请返回 由这些建筑物形成的 天际线 。
    // 每个建筑物的几何信息由数组 buildings 表示，其中三元组 buildings[i] = [lefti, righti, heighti] 表示：
    // lefti 是第 i 座建筑物左边缘的 x 坐标。
    // righti 是第 i 座建筑物右边缘的 x 坐标。
    // heighti 是第 i 座建筑物的高度。
    // 你可以假设所有的建筑都是完美的长方形，在高度为 0 的绝对平坦的表面上。
    // 天际线 应该表示为由 “关键点” 组成的列表，格式 [[x1,y1],[x2,y2],...] ，并按 x 坐标 进行 排序 。
    // 关键点是水平线段的左端点。列表中最后一个点是最右侧建筑物的终点，y 坐标始终为 0 ，仅用于标记天际线的终点。
    // 此外，任何两个相邻建筑物之间的地面都应被视为天际线轮廓的一部分。
    // 注意：输出天际线中不得有连续的相同高度的水平线。
    // 例如 [...[2 3], [4 5], [7 5], [11 5], [12 7]...] 是不正确的答案；
    // 三条高度为 5 的线应该在最终输出中合并为一个：[...[2 3], [4 5], [12 7], ...]
    // 提示：
    // 1 <= buildings.length <= 104
    // 0 <= lefti < righti <= 231 - 1
    // 1 <= heighti <= 231 - 1
    // buildings 按 lefti 非递减排序

    // 方法一：扫描线 + 优先队列-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 枚举建筑的每一个边缘作为关键点的横坐标，检查每一座建筑是否「包含该横坐标」，找到最大高度，即为该关键点的纵坐标。
    // 使用优先队列来优化寻找最大高度的时间，在我们从左到右枚举横坐标的过程中，实时地更新该优先队列即可。
    // 这样无论何时，优先队列的队首元素即为最大高度。
    // 维护优先队列时可以使用「延迟删除」的技巧：
    // 即我们无需每次横坐标改变就立刻将优先队列中所有不符合条件的元素都删除，而只需要保证优先队列的队首元素「包含该横坐标」即可。
    public List<List<Integer>> getSkyline(int[][] buildings) {
        // 建筑的左右两个边缘作为关键点
        List<Integer> boundaries = new ArrayList<Integer>();
        for (int[] building : buildings) {
            boundaries.add(building[0]);
            boundaries.add(building[1]);
        }
        Collections.sort(boundaries);// 按顺序枚举关键点

        PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> b[1] - a[1]);// [right, height] 堆顶为最高建筑
        List<List<Integer>> ret = new ArrayList<List<Integer>>();
        int idx = 0;// 第几个建筑
        for (int boundary : boundaries) {
            // 遍历所有建筑，该循环最多执行n次
            while (idx < buildings.length && buildings[idx][0] <= boundary) {// 当前建筑左边缘小于当前关键点（buildings是按建筑左边缘大小排序的）
                pq.offer(new int[] { buildings[idx][1], buildings[idx][2] });
                idx++;
            }

            // 延迟删除，保证堆顶建筑右边缘大于当前关键点
            while (!pq.isEmpty() && pq.peek()[0] <= boundary)
                pq.poll();

            int maxn = pq.isEmpty() ? 0 : pq.peek()[1];// 当前关键点最大高度

            // 此时为第一个interval时，当前关键点产生变化时
            if (ret.size() == 0 || maxn != ret.get(ret.size() - 1).get(1))
                ret.add(Arrays.asList(boundary, maxn));
        }
        return ret;
    }

    // 方法一：（自己写的）扫描线（记录变化点） + TreeMap（作用相当于优先队列） -时间复杂度：O(nlogn)，空间复杂度：O(n)
    public List<List<Integer>> getSkyline11(int[][] buildings) {
        // 记录变化点
        TreeMap<Integer, List<Integer>> heightChange = new TreeMap<>();
        for (int[] building : buildings) {
            addChange(building[0], building[2], heightChange);// 建筑的左边缘记正高度
            addChange(building[1], -building[2], heightChange);// 建筑的右边缘记负高度
        }

        // 遍历记录的变化点，得到最终答案
        List<List<Integer>> res = new ArrayList<>();
        TreeMap<Integer, Integer> map = new TreeMap<>();
        map.put(0, 1);// 方便处理最后一个interval
        for (Map.Entry<Integer, List<Integer>> entry : heightChange.entrySet()) {
            for (int height : entry.getValue()) {
                if (height > 0)// 正高度+1
                    map.put(height, map.getOrDefault(height, 0) + 1);
                else {// 负高度-1
                    map.put(-height, map.get(-height) - 1);
                    if (map.get(-height) == 0)// 该高度出现次数为0，去除该键值对
                        map.remove(-height);
                }
            }
            int curMax = map.lastKey();// 当前关键点最大高度

            // 此时为第一个interval时，当前关键点产生变化时
            if (res.isEmpty() || curMax != res.get(res.size() - 1).get(1))
                res.add(Arrays.asList(entry.getKey(), curMax));
        }

        return res;
    }

    private void addChange(int key, int value, TreeMap<Integer, List<Integer>> heightChange) {
        if (heightChange.containsKey(key))
            heightChange.get(key).add(value);
        else {
            List<Integer> ans = new ArrayList<>();
            ans.add(value);
            heightChange.put(key, ans);
        }
    }

    // 227.基本计算器II
    // 给你一个字符串表达式 s ，请你实现一个基本计算器来计算并返回它的值。
    // 整数除法仅保留整数部分。
    // 你可以假设给定的表达式总是有效的。所有中间结果将在 [-231, 231 - 1] 的范围内。
    // 注意：不允许使用任何将字符串作为数学表达式计算的内置函数，比如 eval() 。
    // 提示：
    // 1 <= s.length <= 3 * 105
    // s 由整数和算符 ('+', '-', '*', '/') 组成，中间由一些空格隔开
    // s 表示一个 有效表达式
    // 表达式中的所有整数都是非负整数，且在范围 [0, 231 - 1] 内
    // 题目数据保证答案是一个 32-bit 整数

    // 方法一：栈-时间复杂度：O(n)，空间复杂度：O(n)
    // 在第一次遍历时先计算乘除法，最后再统一计算加减法
    public int calculate(String s) {
        Deque<Integer> stack = new ArrayDeque<Integer>();
        char preSign = '+';
        int num = 0;
        int n = s.length();
        for (int i = 0; i < n; ++i) {
            if (Character.isDigit(s.charAt(i)))
                num = num * 10 + s.charAt(i) - '0';

            if (!Character.isDigit(s.charAt(i)) && s.charAt(i) != ' ' || i == n - 1) {
                switch (preSign) {
                    case '+':
                        stack.push(num);
                        break;
                    case '-':
                        stack.push(-num);// 方便最后计算
                        break;
                    case '*':
                        stack.push(stack.pop() * num);
                        break;
                    default:
                        stack.push(stack.pop() / num);
                }
                preSign = s.charAt(i);// 方便后续参考
                num = 0;// 还原
            }
        }

        int ans = 0;
        while (!stack.isEmpty())
            ans += stack.pop();

        return ans;
    }

    // 方法一：（自己写的）栈-时间复杂度：O(n)，空间复杂度：O(n)
    // 使用了两个栈分别存储数字和运算符
    // 在第一次遍历时先计算乘除法，最后再统一计算加减法
    public int calculate11(String s) {
        int n = s.length();
        Deque<Integer> numStk = new ArrayDeque<>();
        Deque<Character> opStk = new ArrayDeque<>();
        int index = 0;
        while (index < n) {
            if (s.charAt(index) == ' ')
                index++;
            else if (Character.isDigit(s.charAt(index))) {
                // 读取数字
                int start = index;
                while (index < n && Character.isDigit(s.charAt(index)))
                    index++;
                int num = Integer.valueOf(s.substring(start, index));

                // 计算乘除法
                if (!numStk.isEmpty() && (opStk.peek() == '*' || opStk.peek() == '/'))
                    num = opStk.pop() == '*' ? numStk.pop() * num : numStk.pop() / num;
                numStk.push(num);
            } else
                opStk.push(s.charAt(index++));
        }

        int res = numStk.pollLast();
        while (!numStk.isEmpty())
            res = opStk.pollLast() == '+' ? res + numStk.pollLast() : res - numStk.pollLast();

        return res;
    }

    // 230.二叉搜索树中第K小的元素
    // 给定一个二叉搜索树的根节点 root ，和一个整数 k ，请你设计一个算法查找其中第 k 个最小元素（从 1 开始计数）。
    // 提示：
    // 树中的节点数为 n 。
    // 1 <= k <= n <= 104
    // 0 <= Node.val <= 104
    // 进阶：如果二叉搜索树经常被修改（插入/删除操作）并且你需要频繁地查找第 k 小的值，你将如何优化算法？

    // 方法一：中序遍历（自己写的，dfs）-时间复杂度：O(n)，空间复杂度：O(n)
    class tsolution_230 {
        int index = 0;
        int k;
        int res;
        boolean find = false;

        public int kthSmallest(TreeNode root, int k) {
            this.k = k;
            dfs(root);
            return res;
        }

        private void dfs(TreeNode root) {
            if (root == null || find)
                return;
            dfs(root.left);
            if (++index == k) {
                res = root.val;
                find = true;
            }
            dfs(root.right);
        }
    }

    // 方法一：中序遍历（迭代）-时间复杂度：O(n)，空间复杂度：O(n)
    public int kthSmallest(TreeNode root, int k) {
        Deque<TreeNode> stack = new ArrayDeque<TreeNode>();
        while (root != null || !stack.isEmpty()) {
            while (root != null) {
                stack.push(root);
                root = root.left;
            }
            root = stack.pop();
            --k;
            if (k == 0)
                break;
            root = root.right;
        }
        return root.val;
    }

    // 方法二： 记录子树的结点数-时间复杂度：O(n)，空间复杂度：O(n)
    // 如果你需要频繁地查找第 k 小的值，你将如何优化算法？
    // 在方法一中，我们之所以需要中序遍历前 k 个元素，是因为我们不知道子树的结点数量，不得不通过遍历子树的方式来获知。
    // 因此，我们可以记录下以每个结点为根结点的子树的结点数，并在查找第 k 小的值时，使用如下方法搜索：
    // 令 node 等于根结点，开始搜索。
    // 对当前结点 node 进行如下操作：
    // 如果 node 的左子树的结点数 left 小于 k−1，则第 k 小的元素一定在 node 的右子树中，
    // 令 node 等于其的右子结点，k 等于 k−left−1，并继续搜索；
    // 如果 node 的左子树的结点数 left 等于 k−1，则第 k 小的元素即为 nodenode ，结束搜索并返回 node 即可；
    // 如果 node 的左子树的结点数 left 大于 k−1，则第 k 小的元素一定在 node 的左子树中，令 node 等于其左子结点，并继续搜索。
    class tsolution_230_2 {
        class MyBst {
            TreeNode root;
            Map<TreeNode, Integer> nodeNum;

            public MyBst(TreeNode root) {
                this.root = root;
                this.nodeNum = new HashMap<TreeNode, Integer>();
                countNodeNum(root);
            }

            // 返回二叉搜索树中第k小的元素
            public int kthSmallest(int k) {
                TreeNode node = root;
                while (node != null) {
                    int left = getNodeNum(node.left);
                    if (left < k - 1) {
                        node = node.right;
                        k -= left + 1;
                    } else if (left == k - 1)
                        break;
                    else
                        node = node.left;

                }
                return node.val;
            }

            // 统计以node为根结点的子树的结点数
            private int countNodeNum(TreeNode node) {
                if (node == null)
                    return 0;

                nodeNum.put(node, 1 + countNodeNum(node.left) + countNodeNum(node.right));
                return nodeNum.get(node);
            }

            // 获取以node为根结点的子树的结点数
            private int getNodeNum(TreeNode node) {
                return nodeNum.getOrDefault(node, 0);
            }
        }

        public int kthSmallest(TreeNode root, int k) {
            MyBst bst = new MyBst(root);
            return bst.kthSmallest(k);
        }
    }

    // 方法三：平衡二叉搜索树-时间复杂度：预处理的时间复杂度为 O(N)，其中 N 是树中结点的总数。
    // 插入、删除和搜索的时间复杂度均为 O(logN)。
    // 空间复杂度：O(N)

    // 如果二叉搜索树经常被修改（插入/删除操作）并且你需要频繁地查找第 k 小的值，你将如何优化算法？
    // 预备知识
    // 方法三需要先掌握 平衡二叉搜索树（AVL树） 的知识。平衡二叉搜索树具有如下性质：
    // 平衡二叉搜索树中每个结点的左子树和右子树的高度最多相差 1；
    // 平衡二叉搜索树的子树也是平衡二叉搜索树；
    // 一棵存有 n 个结点的平衡二叉搜索树的高度是 O(logn)。
    class tsolution_230_3 {
        public int kthSmallest(TreeNode root, int k) {
            // 中序遍历生成数值列表
            List<Integer> inorderList = new ArrayList<Integer>();
            inorder(root, inorderList);

            // 构造平衡二叉搜索树
            AVL avl = new AVL(inorderList);

            // 模拟1000次插入和删除操作
            int[] randomNums = new int[1000];
            Random random = new Random();
            for (int i = 0; i < 1000; ++i) {
                randomNums[i] = random.nextInt(10001);
                avl.insert(randomNums[i]);
            }
            shuffle(randomNums); // 列表乱序
            for (int i = 0; i < 1000; ++i)
                avl.delete(randomNums[i]);

            return avl.kthSmallest(k);
        }

        private void inorder(TreeNode node, List<Integer> inorderList) {
            if (node.left != null)
                inorder(node.left, inorderList);

            inorderList.add(node.val);
            if (node.right != null)
                inorder(node.right, inorderList);
        }

        private void shuffle(int[] arr) {
            Random random = new Random();
            int length = arr.length;
            for (int i = 0; i < length; i++) {
                int randIndex = random.nextInt(length);
                int temp = arr[i];
                arr[i] = arr[randIndex];
                arr[randIndex] = temp;
            }
        }

        // 平衡二叉搜索树（AVL树）：允许重复值
        class AVL {
            Node root;

            // 平衡二叉搜索树结点
            class Node {
                int val;
                Node parent;
                Node left;
                Node right;
                int size;
                int height;

                public Node(int val) {
                    this(val, null);
                }

                public Node(int val, Node parent) {
                    this(val, parent, null, null);
                }

                public Node(int val, Node parent, Node left, Node right) {
                    this.val = val;
                    this.parent = parent;
                    this.left = left;
                    this.right = right;
                    this.height = 0; // 结点高度：以node为根节点的子树的高度（高度定义：叶结点的高度是0）
                    this.size = 1; // 结点元素数：以node为根节点的子树的节点总数
                }
            }

            public AVL(List<Integer> vals) {
                if (vals != null) {
                    this.root = build(vals, 0, vals.size() - 1, null);
                }
            }

            // 根据vals[l:r]构造平衡二叉搜索树 -> 返回根结点
            private Node build(List<Integer> vals, int l, int r, Node parent) {
                int m = (l + r) >> 1;
                Node node = new Node(vals.get(m), parent);
                if (l <= m - 1) {
                    node.left = build(vals, l, m - 1, node);
                }
                if (m + 1 <= r) {
                    node.right = build(vals, m + 1, r, node);
                }
                recompute(node);
                return node;
            }

            // 返回二叉搜索树中第k小的元素
            public int kthSmallest(int k) {
                Node node = root;
                while (node != null) {
                    int left = getSize(node.left);
                    if (left < k - 1) {
                        node = node.right;
                        k -= left + 1;
                    } else if (left == k - 1) {
                        break;
                    } else {
                        node = node.left;
                    }
                }
                return node.val;
            }

            public void insert(int v) {
                if (root == null)
                    root = new Node(v);
                else {
                    // 计算新结点的添加位置
                    Node node = subtreeSearch(root, v);
                    boolean isAddLeft = v <= node.val; // 是否将新结点添加到node的左子结点
                    if (node.val == v) { // 如果值为v的结点已存在
                        if (node.left != null) { // 值为v的结点存在左子结点，则添加到其左子树的最右侧
                            node = subtreeLast(node.left);
                            isAddLeft = false;
                        } else // 值为v的结点不存在左子结点，则添加到其左子结点
                            isAddLeft = true;

                    }

                    // 添加新结点
                    Node leaf = new Node(v, node);
                    if (isAddLeft)
                        node.left = leaf;
                    else
                        node.right = leaf;

                    rebalance(leaf);
                }
            }

            // 删除值为v的结点 -> 返回是否成功删除结点
            public boolean delete(int v) {
                if (root == null)
                    return false;

                Node node = subtreeSearch(root, v);
                if (node.val != v) // 没有找到需要删除的结点
                    return false;

                // 处理当前结点既有左子树也有右子树的情况
                // 若左子树比右子树高度低，则将当前结点替换为右子树最左侧的结点，并移除右子树最左侧的结点
                // 若右子树比左子树高度低，则将当前结点替换为左子树最右侧的结点，并移除左子树最右侧的结点
                if (node.left != null && node.right != null) {
                    Node replacement = null;
                    if (node.left.height <= node.right.height)
                        replacement = subtreeFirst(node.right);
                    else
                        replacement = subtreeLast(node.left);

                    node.val = replacement.val;
                    node = replacement;
                }

                Node parent = node.parent;
                delete(node);
                rebalance(parent);
                return true;
            }

            // 删除结点p并用它的子结点代替它，结点p至多只能有1个子结点
            private void delete(Node node) {
                if (node.left != null && node.right != null) {
                    return;
                    // throw new Exception("Node has two children");
                }
                Node child = node.left != null ? node.left : node.right;
                if (child != null)
                    child.parent = node.parent;

                if (node == root)
                    root = child;
                else {
                    Node parent = node.parent;
                    if (node == parent.left)
                        parent.left = child;
                    else
                        parent.right = child;

                }
                node.parent = node;
            }

            // 在以node为根结点的子树中搜索值为v的结点，如果没有值为v的结点，则返回值为v的结点应该在的位置的父结点
            private Node subtreeSearch(Node node, int v) {
                if (node.val < v && node.right != null)
                    return subtreeSearch(node.right, v);
                else if (node.val > v && node.left != null)
                    return subtreeSearch(node.left, v);
                else
                    return node;

            }

            // 重新计算node结点的高度和元素数
            private void recompute(Node node) {
                node.height = 1 + Math.max(getHeight(node.left), getHeight(node.right));
                node.size = 1 + getSize(node.left) + getSize(node.right);
            }

            // 从node结点开始（含node结点）逐个向上重新平衡二叉树，并更新结点高度和元素数
            private void rebalance(Node node) {
                while (node != null) {
                    int oldHeight = node.height, oldSize = node.size;
                    if (!isBalanced(node)) {
                        node = restructure(tallGrandchild(node));
                        recompute(node.left);
                        recompute(node.right);
                    }
                    recompute(node);
                    if (node.height == oldHeight && node.size == oldSize)
                        node = null; // 如果结点高度和元素数都没有变化则不需要再继续向上调整
                    else
                        node = node.parent;

                }
            }

            // 判断node结点是否平衡
            private boolean isBalanced(Node node) {
                return Math.abs(getHeight(node.left) - getHeight(node.right)) <= 1;
            }

            // 获取node结点更高的子树
            private Node tallChild(Node node) {
                if (getHeight(node.left) > getHeight(node.right))
                    return node.left;
                else
                    return node.right;

            }

            // 获取node结点更高的子树中的更高的子树
            private Node tallGrandchild(Node node) {
                Node child = tallChild(node);
                return tallChild(child);
            }

            // 重新连接父结点和子结点（子结点允许为空）
            private static void relink(Node parent, Node child, boolean isLeft) {
                if (isLeft)
                    parent.left = child;
                else
                    parent.right = child;

                if (child != null)
                    child.parent = parent;

            }

            // 旋转操作
            private void rotate(Node node) {
                Node parent = node.parent;
                Node grandparent = parent.parent;
                if (grandparent == null) {
                    root = node;
                    node.parent = null;
                } else
                    relink(grandparent, node, parent == grandparent.left);

                if (node == parent.left) {
                    relink(parent, node.right, true);
                    relink(node, parent, false);
                } else {
                    relink(parent, node.left, false);
                    relink(node, parent, true);
                }
            }

            // trinode操作
            private Node restructure(Node node) {
                Node parent = node.parent;
                Node grandparent = parent.parent;

                if ((node == parent.right) == (parent == grandparent.right)) { // 处理需要一次旋转的情况
                    rotate(parent);
                    return parent;
                } else { // 处理需要两次旋转的情况：第1次旋转后即成为需要一次旋转的情况
                    rotate(node);
                    rotate(node);
                    return node;
                }
            }

            // 返回以node为根结点的子树的第1个元素
            private static Node subtreeFirst(Node node) {
                while (node.left != null)
                    node = node.left;

                return node;
            }

            // 返回以node为根结点的子树的最后1个元素
            private static Node subtreeLast(Node node) {
                while (node.right != null)
                    node = node.right;

                return node;
            }

            // 获取以node为根结点的子树的高度
            private static int getHeight(Node node) {
                return node != null ? node.height : 0;
            }

            // 获取以node为根结点的子树的结点数
            private static int getSize(Node node) {
                return node != null ? node.size : 0;
            }
        }
    }

    // 234.回文链表
    // 给你一个单链表的头节点 head ，请你判断该链表是否为回文链表。如果是，返回 true ；否则，返回 false 。
    // 提示：
    // 链表中节点数目在范围[1, 105] 内
    // 0 <= Node.val <= 9
    // 进阶：你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题？

    // 方法一：将值复制到数组中后用双指针法-空间复杂度O(n)
    public boolean isPalindrome(ListNode head) {
        List<Integer> vals = new ArrayList<Integer>();

        // 将链表的值复制到数组中
        ListNode currentNode = head;
        while (currentNode != null) {
            vals.add(currentNode.val);
            currentNode = currentNode.next;
        }

        // 使用双指针判断是否回文
        int front = 0;
        int back = vals.size() - 1;
        while (front < back) {
            if (!vals.get(front).equals(vals.get(back))) {
                return false;
            }
            front++;
            back--;
        }
        return true;
    }

    // 方法二：递归-空间复杂度O(n)
    // 递归特性：1.为空指针（开始掉头），2.不为空指针
    private ListNode frontPointer;

    public boolean isPalindrome2(ListNode head) {
        frontPointer = head; // 全局变量，从头结点开始遍历
        return recursivelyCheck(head);
    }

    private boolean recursivelyCheck(ListNode currentNode) {
        if (currentNode != null) {// 递归到末尾结点时，指向NULL，return true出栈，到倒数第二个结点
            if (!recursivelyCheck(currentNode.next)) // 栈顶结点已不匹配，则无需对比
                return false;
            if (currentNode.val != frontPointer.val)
                return false;

            frontPointer = frontPointer.next;// 能到该句，一定是之前都成功匹配，前结点向后移
        }
        return true;// 能到该句，一定是之前都成功匹配，返回true，直到前结点至末，后结点至首（空链表也为回文）
    }

    // 方法三：快慢指针（反转链表）-空间复杂度O(1)
    class ssoltion_234_3 {
        public boolean isPalindrome(ListNode head) {
            if (head == null) {
                return true;
            }
            // 1→2→3→4→NULL 1→2→3 NULL←3←4
            // 找到前半部分链表的尾节点并反转后半部分链表
            ListNode firstHalfEnd = endOfFirstHalf(head);// 前半部分链表尾结点
            ListNode secondHalfStart = reverseList(firstHalfEnd.next);// 后半部分链表尾结点，即新后半部分开始

            // 判断是否回文
            ListNode p1 = head;
            ListNode p2 = secondHalfStart;
            while (p2 != null) {// p2==NULL时，p1==firstHalfEnd->next，分情况讨论下，链表奇偶数个节点就明朗了
                if (p1.val != p2.val)
                    return false;

                p1 = p1.next;
                p2 = p2.next;
            }

            // 还原链表并返回结果
            firstHalfEnd.next = reverseList(secondHalfStart);
            return true;
        }

        // 反转链表
        private ListNode reverseList(ListNode head) {
            ListNode prev = null;
            ListNode curr = head;
            while (curr != null) {
                ListNode nextTemp = curr.next;// 存储当前结点指向的下一结点位置
                curr.next = prev;// 当前结点指向前驱结点
                prev = curr;
                curr = nextTemp;
            }
            return prev;// 跳出循环时curr==NULL，则prev为末结点，即反转链表后的头结点
        }

        // 快慢指针-找到前半部分链表的尾结点
        // 1→2→3→4→NULL
        // 1→2→3→NULL
        private ListNode endOfFirstHalf(ListNode head) {
            ListNode fast = head;
            ListNode slow = head;
            while (fast.next != null && fast.next.next != null) {
                fast = fast.next.next;
                slow = slow.next;
            }
            return slow;
        }
    }

    // 236.二叉树的最近公共祖先
    // 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先
    // 公共祖先的定义为：“对于有根树 T 的两个节点 p、q，最近公共祖先表示为一个节点 x
    // 满足 x 是 p、q 的祖先且 x 的深度尽可能大（一个节点也可以是它自己的祖先）
    // 树中节点数目在范围 [2, 105] 内
    // -109 <= Node.val <= 109
    // 所有 Node.val 「互不相同」
    // p != q
    // p 和 q 均存在于给定的二叉树中

    // 方法一：递归DFS（后序遍历）-时间复杂度，空间复杂度-O(n)
    // 递归遍历整棵二叉树，定义 fx 表示 x 节点的子树中是否包含 p 节点或 q 节点，如果包含为 true，否则为 false
    // 那么符合条件的最近公共祖先 x 一定满足如下条件：
    // (flson && frson) ||  ((x = p || x = q) && (flson || frson))
    // flson && frson一定符合
    // (x = p || x = q) && (flson || frson)同时满足也符合，即父节点是p或q且另一个在其左或右子树下
    class tsolution_236 {
        TreeNode ans = null;

        boolean dfs(TreeNode root, TreeNode p, TreeNode q) {
            if (root == null)
                return false;
            boolean lson = dfs(root.left, p, q);// p q 是否在左子树下
            boolean rson = dfs(root.right, p, q);// p q 是否在右子树下
            if ((lson && rson) || ((root.val == p.val || root.val == q.val) && (lson || rson)))
                ans = root;

            return lson || rson || (root.val == p.val || root.val == q.val);// p q是否在该节点下，或该节点就是p q
        }

        TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
            this.dfs(root, p, q);
            return ans;
        }
    }

    // 方法二：存储父节点-时间复杂度，空间复杂度-O(n)
    // 不是顺序二叉树，不能简单通过序号来判断是否是父节点
    class tsolution_236_2 {
        Map<Integer, TreeNode> parent = new HashMap<Integer, TreeNode>();// 键值对，key为节点val，value为该节点的父节点
        Set<Integer> visited = new HashSet<Integer>();// 所有节点p的祖先节点（包括p自身）

        void dfs(TreeNode root) {
            if (root.left != null) {
                parent.put(root.left.val, root);
                dfs(root.left);
            }
            if (root.right != null) {
                parent.put(root.right.val, root);
                dfs(root.right);
            }
        }

        TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
            dfs(root);
            while (p != null) {
                visited.add(p.val);// p的所有父节点，包括p节点自己
                p = parent.get(p.val);// p的父节点
            }
            while (q != null) {
                if (visited.contains(q.val))// 找到共同的祖先节点
                    return q;
                q = parent.get(q.val);
            }
            return null;
        }
    }

    // 237.删除链表中的节点
    // 有一个单链表的 head，我们想删除它其中的一个节点 node。
    // 给你一个需要删除的节点 node 。你将 无法访问 第一个节点  head。
    // 链表的所有值都是 唯一的，并且保证给定的节点 node 不是链表中的最后一个节点。
    // 删除给定的节点。注意，删除节点并不是指从内存中删除它。这里的意思是：
    // 给定节点的值不应该存在于链表中。
    // 链表中的节点数应该减少 1。
    // node 前面的所有值顺序相同。
    // node 后面的所有值顺序相同。
    // 提示：
    // 链表中节点的数目范围是 [2, 1000]
    // -1000 <= Node.val <= 1000
    // 链表中每个节点的值都是 唯一 的
    // 需要删除的节点 node 是 链表中的节点 ，且 不是末尾节点

    // 方法一：和下一个节点交换
    // 无法删除末尾节点（提示中也有避开末尾节点）
    public void deleteNode(ListNode node) {
        node.val = node.next.val;
        node.next = node.next.next;
    }

    // 238.除自身以外数组的乘积
    // 给你一个长度为 n 的整数数组 nums，其中 n > 1，返回输出数组 output ，
    // 其中 output[i] 等于 nums 中除 nums[i] 之外其余各元素的乘积。
    // 提示：题目数据保证数组之中任意元素的全部前缀元素和后缀（甚至是整个数组）的乘积都在 32 位整数范围内。
    // 说明: 请「不要使用除法」，且在 O(n) 时间复杂度内完成此题。
    // 进阶：
    // 你可以在常数空间复杂度内完成这个题目吗？（ 出于对空间复杂度分析的目的，输出数组不被视为额外空间。）

    // 方法一：左右乘积列表-时间复杂度：O(n)，空间复杂度：O(n)
    // 数组 L 和 R。对于给定索引 i，L[i] 代表的是 i 左侧所有数字的乘积，R[i] 代表的是 i 右侧所有数字的乘积。
    public int[] productExceptSelf(int[] nums) {
        int length = nums.length;

        // L 和 R 分别表示左右两侧的乘积列表
        int[] L = new int[length];
        int[] R = new int[length];
        int[] answer = new int[length];

        // L[i] 为索引 i 左侧所有元素的乘积
        // 对于索引为 '0' 的元素，因为左侧没有元素，所以 L[0] = 1
        L[0] = 1;
        for (int i = 1; i < length; i++)
            L[i] = nums[i - 1] * L[i - 1];

        // R[i] 为索引 i 右侧所有元素的乘积
        // 对于索引为 'length-1' 的元素，因为右侧没有元素，所以 R[length-1] = 1
        R[length - 1] = 1;
        for (int i = length - 2; i >= 0; i--)
            R[i] = nums[i + 1] * R[i + 1];

        // 对于索引 i，除 nums[i] 之外其余各元素的乘积就是左侧所有元素的乘积乘以右侧所有元素的乘积
        for (int i = 0; i < length; i++)
            answer[i] = L[i] * R[i];

        return answer;
    }

    // 方法二：找规律-时间复杂度：O(n)，空间复杂度：O(1)
    // 先把输出数组当作 L 数组来计算，然后再动态构造 R 数组得到结果
    public int[] productExceptSelf2(int[] nums) {
        int length = nums.length;
        int[] answer = new int[length];

        // answer[i] 表示索引 i 左侧所有元素的乘积
        // 因为索引为 '0' 的元素左侧没有元素， 所以 answer[0] = 1
        answer[0] = 1;
        for (int i = 1; i < length; i++)
            answer[i] = nums[i - 1] * answer[i - 1];

        // R 为右侧所有元素的乘积
        // 刚开始右边没有元素，所以 R = 1
        int R = 1;
        for (int i = length - 1; i >= 0; i--) {
            // 对于索引 i，左边的乘积为 answer[i]，右边的乘积为 R
            answer[i] = answer[i] * R;
            // R 需要包含右边所有的乘积，所以计算下一个结果时需要将当前值乘到 R 上
            R *= nums[i];
        }
        return answer;
    }

    // 方法二：（自己写的）找规律-时间复杂度：O(n)，空间复杂度：O(1)
    public int[] productExceptSelf22(int[] nums) {
        int n = nums.length;
        int[] res = new int[n];
        Arrays.fill(res, 1);

        int leftProduct = 1;
        for (int i = 1; i < n; i++) {
            leftProduct *= nums[i - 1];
            res[i] *= leftProduct;
        }

        int rightProduct = 1;
        for (int i = n - 2; i >= 0; i--) {
            rightProduct *= nums[i + 1];
            res[i] *= rightProduct;
        }

        return res;
    }

    // 239.滑动窗口最大值
    // 方法一：优先队列-时间复杂度：O(nlogn)，将一个元素放入优先队列的时间复杂度为 O(logn)
    // （该题只需要求窗口内最大值，并不关系整个窗口内的排序，使用堆一定程度上做了无用功，同理二叉排序树）
    // 空间复杂度：O(n)
    // 在优先队列中存储二元组 (num,index)，表示元素 num 在数组中的下标为 index
    public int[] maxSlidingWindow(int[] nums, int k) {
        int n = nums.length;
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>(
                (pair1, pair2) -> pair1[0] != pair2[0] ? pair2[0] - pair1[0] : pair2[1] - pair1[1]);

        // 初始时，我们将数组 nums 的前 k 个元素放入优先队列中
        for (int i = 0; i < k; ++i)
            pq.offer(new int[] { nums[i], i });

        int[] ans = new int[n - k + 1];
        ans[0] = pq.peek()[0];// 堆顶即最大值
        for (int i = k; i < n; ++i) {
            pq.offer(new int[] { nums[i], i });// 新元素入队
            while (pq.peek()[1] <= i - k) // 堆顶元素已不在窗口内（要循环判断，直到堆顶元素在窗口内）
                pq.poll();// 出队

            ans[i - k + 1] = pq.peek()[0];// 堆顶即最大值
        }
        return ans;
    }

    // 方法二：单调（双端）队列-时间复杂度：O(n)，空间复杂度：O(k)
    // 单调队列存放索引值，按元素大小严格单调递减
    public int[] maxSlidingWindow2(int[] nums, int k) {
        int n = nums.length;
        Deque<Integer> deque = new LinkedList<Integer>(); // 单调（双端）队列
        // 初始时，我们将数组 nums 的前 k 个元素放入单调队列中
        for (int i = 0; i < k; ++i) {
            while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()])// 队列非空且当前入队元素比队尾元素大
                deque.pollLast();// 队尾元素的索引出队
            deque.offerLast(i);// 插入至队尾
        }

        int[] ans = new int[n - k + 1];
        ans[0] = nums[deque.peekFirst()];// 队头即最大值
        for (int i = k; i < n; ++i) {
            while (!deque.isEmpty() && nums[i] >= nums[deque.peekLast()])
                deque.pollLast();
            deque.offerLast(i);

            while (deque.peekFirst() <= i - k) // 队头元素已不在窗口内（要循环判断，直到队头元素在窗口内）
                deque.pollFirst();

            ans[i - k + 1] = nums[deque.peekFirst()];// 队头即最大值
        }
        return ans;
    }

    // 方法三：分块 + 预处理-时空复杂度：O(n)
    // 我们可以将数组 nums 从左到右按照 k 个一组进行分组，最后一组中元素的数量可能会不足 k 个。
    // 那么 nums[i] 到 nums[i+k−1] 会跨越两个分组，占有第一个分组的后缀以及第二个分组的前缀。
    // 如果我们能够预处理出每个分组中的前缀最大值以及后缀最大值，同样可以在 O(1) 的时间得到答案。
    // 这种方法与稀疏表（Sparse Table）非常类似
    // 分块边界一定要相对应
    public int[] maxSlidingWindow3(int[] nums, int k) {
        int n = nums.length;
        int[] prefixMax = new int[n];
        int[] suffixMax = new int[n];

        for (int i = 0; i < n; ++i) {
            if (i % k == 0)
                prefixMax[i] = nums[i];
            else
                prefixMax[i] = Math.max(prefixMax[i - 1], nums[i]);

        }
        for (int i = n - 1; i >= 0; --i) {
            if (i == n - 1 || (i + 1) % k == 0)
                suffixMax[i] = nums[i];
            else
                suffixMax[i] = Math.max(suffixMax[i + 1], nums[i]);

        }

        int[] ans = new int[n - k + 1];
        for (int i = 0; i <= n - k; ++i)
            ans[i] = Math.max(suffixMax[i], prefixMax[i + k - 1]);

        return ans;
    }

    // 240.搜索二维矩阵II
    // 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性：
    // 每行的元素从左到右升序排列。
    // 每列的元素从上到下升序排列。
    // 提示：
    // m == matrix.length
    // n == matrix[i].length
    // 1 <= n, m <= 300
    // -109 <= matrix[i][j] <= 109
    // -109 <= target <= 109

    // 方法一：直接查找-时间复杂度：O(mn)
    // 方法二：二分查找-时间复杂度：O(mlogn)
    // 由于矩阵 matrix 中每一行的元素都是升序排列的，因此我们可以对每一行都使用一次二分查找
    public boolean searchMatrix2(int[][] matrix, int target) {
        for (int[] row : matrix) {
            int index = biSearch(row, target);
            if (index >= 0)
                return true;

        }
        return false;
    }

    public int biSearch(int[] nums, int target) {
        int low = 0, high = nums.length - 1;
        while (low <= high) {
            int mid = (high - low) / 2 + low;
            int num = nums[mid];
            if (num == target)
                return mid;
            else if (num > target)
                high = mid - 1;
            else
                low = mid + 1;

        }
        return -1;
    }

    // 方法三：Z 字形查找-时间复杂度：O(m+n)，空间复杂度：O(1)
    // 从矩阵 matrix 的右上角 (0, n-1) 进行搜索。在每一步的搜索过程中，如果我们位于位置 (x, y)，
    // 那么我们希望在以 matrix 的左下角为左下角、以 (x, y) 为右上角的矩阵中进行搜索，
    // 即行的范围为 [x, m - 1]，列的范围为 [0, y]：
    public boolean searchMatrix3(int[][] matrix, int target) {
        int m = matrix.length, n = matrix[0].length;
        int x = 0, y = n - 1;
        while (x < m && y >= 0) {
            if (matrix[x][y] == target)
                return true;

            if (matrix[x][y] > target)// matrix[x][y]往下也一定大于target，丢弃该列
                --y;
            else// matrix[x][y]往左一定小于target，丢弃该行
                ++x;

        }
        return false;
    }

    // 242.有效的字母异位词
    // 给定两个字符串 s 和 t ，编写一个函数来判断 t 是否是 s 的字母异位词。
    // 注意：若 s 和 t 中每个字符出现的次数都相同，则称 s 和 t 互为字母异位词。
    // 提示:
    // 1 <= s.length, t.length <= 5 * 104
    // s 和 t 仅包含小写字母
    // 进阶: 如果输入字符串包含 unicode 字符怎么办？你能否调整你的解法来应对这种情况？

    // 方法一：排序-时间复杂度：O(nlogn)，空间复杂度：O(logn)
    // 对字符串 s 和 t 分别排序，看排序后的字符串是否相等

    // 方法二：哈希表-时间复杂度：O(n)，空间复杂度：O(S)，其中 S 为字符集大小
    // 由于字符串只包含 26 个小写字母，因此我们可以维护一个长度为 26 的频次数组 table
    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length())
            return false;

        int[] table = new int[26];
        for (int i = 0; i < s.length(); i++)
            table[s.charAt(i) - 'a']++;

        for (int i = 0; i < t.length(); i++) {
            table[t.charAt(i) - 'a']--;
            if (table[t.charAt(i) - 'a'] < 0)
                return false;
        }
        return true;
    }

    // 方法二：（进阶问题：输入字符串包含 unicode 字符）哈希表-时间复杂度：O(n)，空间复杂度：O(S)，其中 S 为字符集大小
    public boolean isAnagram22(String s, String t) {
        if (s.length() != t.length())
            return false;

        Map<Character, Integer> table = new HashMap<Character, Integer>();
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            table.put(ch, table.getOrDefault(ch, 0) + 1);
        }
        for (int i = 0; i < t.length(); i++) {
            char ch = t.charAt(i);
            table.put(ch, table.getOrDefault(ch, 0) - 1);
            if (table.get(ch) < 0)
                return false;
        }
        return true;
    }

    // 251.展开二维向量
    // 请设计并实现一个能够展开二维向量的迭代器。该迭代器需要支持 next 和 hasNext 两种操作。
    // 实例：
    // Vector2D iterator = new Vector2D([[1,2],[3],[4]]);
    // iterator.next(); // 返回 1
    // iterator.next(); // 返回 2
    // iterator.next(); // 返回 3
    // iterator.hasNext(); // 返回 true
    // iterator.hasNext(); // 返回 true
    // iterator.next(); // 返回 4
    // iterator.hasNext(); // 返回 false
    // 注意:
    // 1.请记得重置在Vector2D中声明的类变量(静态变量)，因为类变量会在多个测试用例中保持不变，影响判题准确。请查阅这里。
    // 2.你可以假定next() 的调用总是合法的，即当next() 被调用时，二维向量总是存在至少一个后续元素。
    // 进阶:
    // 尝试在代码中仅使用C++提供的迭代器或Java提供的迭代器。

    // 方法一：提前展开至一维数组-时间复杂度：O(n)，空间复杂度：O(n)
    class Vector2D {
        int[] arr = null;
        int cur = 0;

        public Vector2D(int[][] vec) {
            int count = 0;
            for (int[] ints : vec)
                count += ints.length;
            arr = new int[count];
            int index = 0;
            for (int[] ints : vec) {
                System.arraycopy(ints, 0, arr, index, ints.length);
                index += ints.length;
            }
        }

        public int next() {
            return arr[cur++];
        }

        public boolean hasNext() {
            return cur < arr.length;
        }
    }

    // 方法二：逐层遍历-时间复杂度：O(n)，空间复杂度：O(1)
    class Vector2D_2 {
        private int[][] vector;
        private int inner = 0;
        private int outer = 0;

        public Vector2D_2(int[][] vec) {
            vector = vec;
        }

        public int next() {
            if (inner < vector[outer].length)
                return vector[outer][inner++];
            inner = 0;
            outer++;
            return vector[outer][inner];
        }

        public boolean hasNext() {
            if (outer < vector.length || inner < vector[outer].length)
                return true;
            return false;
        }
    }

    // 253.会议室 II
    // 给定一个会议时间安排的数组，每个会议时间都会包括开始和结束的时间 [[s1,e1],[s2,e2],…] (si < ei)
    // 为避免会议冲突，同时要考虑充分利用会议室资源，请计算至少需要多少间会议室，才能满足这些会议安排

    // 方法一：排序 + 优先队列、最小堆-时间复杂度：O(nlogn)
    // 各会议按开始时间排序，每当有新会议时，就遍历所有房间，如果有空房间
    // （新会议的开始时间 >= 堆顶会议的结束时间：因为堆顶是最早结束的，如果它还不结束，则其他的都不结束），不用开新房间了
    // 更新旧房间的结束时间，否则开新房间。使用优先队列，最小堆，堆顶为结束时间最早的会议
    // arr[第几个会议][0开始时间 1结束时间]
    public int minMeetingRooms(int[][] arr) {
        if (arr.length == 0)
            return 0;

        // 优先队列，最小堆，堆顶为结束时间最早的会议，存放的是各房间的结束时间
        PriorityQueue<Integer> q = new PriorityQueue<>();
        Arrays.sort(arr, (meeting1, meeting2) -> meeting1[0] - meeting2[0]);// 按照开始时间排序，增添新房间时，一定是找不到已有房间安排当前会议
        q.offer(arr[0][1]);// 第一个会议的结束时间入队

        for (int i = 1; i < arr.length; i++) {
            System.out.println(q.peek());
            if (arr[i][0] >= q.peek())// 新会议的开始时间 >= 堆顶会议的结束时间（堆顶是最早结束的），更新旧房间的结束时间
                q.poll();// 堆顶房间出队
            q.offer(arr[i][1]);// 更新出队房间的结束时间，或新房间入队
        }
        return q.size();// 队列中的元素数即房间数
    }

    // 方法二：排序 + 双指针法-时间复杂度：O(nlogn)
    // 统计同一时刻进行的最大会议的数量（也即需要的最多房间），分别按照开始时间和结束时间排序，遍历两个数组
    public int minMeetingRooms2(int[][] arr) {
        int[] begin = new int[arr.length];
        int[] end = new int[arr.length];
        for (int i = 0; i < arr.length; i++) {
            begin[i] = arr[i][0];
            end[i] = arr[i][1];
        }
        Arrays.sort(begin);
        Arrays.sort(end);
        int i = 0, j = 0;// 分别指向开始时间和结束时间的下标
        int max = 0;// 同一时刻进行的最大会议的数量
        int cur = 0;// 当前时刻进行的最大会议的数量，第j场会议结束前有多少场会议开始了（肯定包含第j场会议本身）
        while (i < arr.length && j < arr.length) {
            if (begin[i] < end[j]) {
                cur++;
                i++;
            } else if (begin[i] == end[j]) {
                i++;
                j++;
            } else {// begin[i] > end[j]
                cur--;
                j++;
            }
            max = Math.max(max, cur);
        }
        return max;

    }

    // 268.丢失的数字
    // 给定一个包含 [0, n] 中 n 个数的数组 nums ，找出 [0, n] 这个范围内没有出现在数组中的那个数。
    // 进阶：你能否实现线性时间复杂度、仅使用额外常数空间的算法解决此问题?

    // 方法一：排序-时间复杂度：O(nlogn)，空间复杂度：O(logn)
    // 将数组排序之后，即可根据数组中每个下标处的元素是否和下标相等，得到丢失的数字。

    // 方法二：哈希集合-时间复杂度：O(n)，空间复杂度：O(n)

    // 方法三：位运算-时间复杂度：O(n)，空间复杂度：O(1)
    public int missingNumber3(int[] nums) {
        int xor = 0;
        int n = nums.length;
        for (int i = 0; i < n; i++)
            xor ^= nums[i];

        for (int i = 0; i <= n; i++)
            xor ^= i;

        return xor;
    }

    // 方法四：数学-时间复杂度：O(n)，空间复杂度：O(1)
    public int missingNumber4(int[] nums) {
        int n = nums.length;
        int total = n * (n + 1) / 2;
        int arrSum = 0;
        for (int i = 0; i < n; i++)
            arrSum += nums[i];
        return total - arrSum;
    }

    // 方法五：（自己写的）原地修改-时间复杂度：O(n)，空间复杂度：O(1)
    public int missingNumber(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            int num = nums[i];
            if (num > n)
                num = num - n - 1;
            if (num != n)
                nums[num] += n + 1;
        }

        for (int i = 0; i < n; i++)
            if (nums[i] < n + 1)
                return i;
        return n;
    }

    // 269.火星词典
    // 现有一种使用英语字母的外星文语言，这门语言的字母顺序与英语顺序不同。
    // 给定一个字符串列表 words ，作为这门语言的词典，words 中的字符串已经 按这门新语言的字母顺序进行了排序 。
    // 请你根据该词典还原出此语言中已知的字母顺序，并 按字母递增顺序 排列。若不存在合法字母顺序，返回 "" 。
    // 若存在多种可能的合法字母顺序，返回其中 任意一种 顺序即可。
    // 字符串 s 字典顺序小于 字符串 t 有两种情况：
    // 在第一个不同字母处，如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前，那么 s 的字典顺序小于 t 。
    // 如果前面 min(s.length, t.length) 字母都相同，那么 s.length < t.length 时，s 的字典顺序也小于 t 。
    // 提示：
    // 1 <= words.length <= 100
    // 1 <= words[i].length <= 100
    // words[i] 仅由小写英文字母组成

    // 现有一种使用英语字母的外星文语言，这门语言的字母顺序与英语顺序不同。
    // 给定一个字符串列表 words ，作为这门语言的词典，words 中的字符串已经 按这门新语言的字母顺序进行了排序 。
    // 请你根据该词典还原出此语言中已知的字母顺序，并 按字母递增顺序 排列。若不存在合法字母顺序，返回 "" 。
    // 若存在多种可能的合法字母顺序，返回其中 任意一种 顺序即可。
    // 字符串 s 字典顺序小于 字符串 t 有两种情况：
    // 在第一个不同字母处，如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前，那么 s 的字典顺序小于 t 。
    // 如果前面 min(s.length, t.length) 字母都相同，那么 s.length < t.length 时，s 的字典顺序也小于 t 。
    // 提示：
    // 1 <= words.length <= 100
    // 1 <= words[i].length <= 100
    // words[i] 仅由小写英文字母组成

    // 方法一：bfs 拓扑排序
    public String alienOrder(String[] words) {
        // 把出现的字符保存在图HashMap里，每个不重复字符对应一个表示有序的有向边
        Map<Character, Set<Character>> graph = new HashMap<>();// <c1, (c2, c3)> → c1 < c2, c1 < c3
        // List<Character> graph[] = new ArrayList[26];//也可以这样存储图，未出现的字符对应null
        int[] inDegree = new int[26];// 各字符节点入度
        Queue<Character> q = new LinkedList<>();
        StringBuilder sb = new StringBuilder();

        // 记录出现的字符
        for (String word : words)
            for (char ch : word.toCharArray())
                graph.putIfAbsent(ch, new HashSet<>());

        // 两两比较相邻字符串之间的关系，得到两个字符的相对大小关系
        for (int i = 1; i < words.length; ++i) {
            String w1 = words[i - 1];
            String w2 = words[i];
            if (!preCheck(w1, w2))// 不合法 e.g. abc < ab
                return "";
            for (int j = 0; j < Math.min(w1.length(), w2.length()); ++j) {
                char c1 = w1.charAt(j);
                char c2 = w2.charAt(j);
                if (c1 != c2) {// 找到不同的字符才说明有顺序关系，把前一个字符指向后一字符，同时后一字符的入度+1
                    if (!graph.get(c1).contains(c2)) {
                        graph.get(c1).add(c2);
                        inDegree[c2 - 'a']++;
                    }
                    break;
                }
            }
        }
        // 把所有入度为0的字符先加入队列，准备拓扑排序
        for (char ch : graph.keySet())
            if (inDegree[ch - 'a'] == 0)
                q.offer(ch);

        while (!q.isEmpty()) {
            // 从队列出来的这个字符肯定是入度为0，可以确定它的顺序，就把它加进字符顺序的结果里
            char node = q.poll();
            sb.append(node);

            // 该节点指向的所有节点的入度都要减1，若有入度为0的节点字符出现，把它加进队列准备之后的拓扑排序遍历
            for (char next : graph.get(node)) {
                inDegree[next - 'a']--;
                if (inDegree[next - 'a'] == 0)
                    q.offer(next);
            }
        }
        // 如果结果集里的字符数量和图中所有节点数量相同，说明拓扑排序成功，返回结果集，否则不成功，不存在合法字母顺序
        return sb.length() == graph.size() ? sb.toString() : "";
    }

    /**
     * 判断两字符大小关系是否合法（即最大前缀相同，且左边长度大于右边 e.g. abc < ab 不合法）
     * 
     * @param s1
     * @param s2
     * @return
     */
    public boolean preCheck(String s1, String s2) {
        int m = s1.length(), n = s2.length();
        if (m < n)// 左字符串长度小于右字符串长度，一定合法
            return true;
        int i = 0, j = 0;
        while (i < m && j < n) {
            if (s2.charAt(j) != s1.charAt(i))
                return true;
            i++;
            j++;
        }
        if (!s1.equals(s2))
            return false;
        else
            return true;
    }

    // 方法一：（自己写的）bfs 拓扑排序
    class ssoltion_269_11 {
        Map<Character, Integer> indegrees = new HashMap<>();
        Map<Character, List<Character>> graph = new HashMap<>();
        // 标记 word 之间出现了不可能的大小顺序，如 abc ab
        boolean valid = true;

        public String alienOrder11(String[] words) {
            // 仅记录有哪些字符，初始化 indegrees graph
            for (String word : words)
                for (char c : word.toCharArray()) {
                    if (!graph.containsKey(c))
                        graph.put(c, new ArrayList<>());
                    if (!indegrees.containsKey(c))
                        indegrees.put(c, 0);
                }

            // 记录 word 两两之间的大小关系，构建图，记录入度
            for (int i = 0; i < words.length - 1; i++) {
                char[] order = getOrder(words[i], words[i + 1]);
                if (!valid)// 标记 word 之间出现了不可能的大小顺序，如 abc ab
                    return "";
                if (order == null)
                    continue;
                char c1 = order[0];
                char c2 = order[1];
                graph.get(c1).add(c2);
                indegrees.put(c2, indegrees.get(c2) + 1);
            }

            // 入度为0的字符加入队列
            Queue<Character> queue = new ArrayDeque<>();
            for (Character c : indegrees.keySet())
                if (indegrees.get(c) == 0)
                    queue.offer(c);

            // bfs 遍历图，得到拓扑排序
            StringBuilder res = new StringBuilder();
            while (!queue.isEmpty()) {
                char c = queue.poll();
                res.append(c);
                for (char next : graph.get(c)) {
                    indegrees.put(next, indegrees.get(next) - 1);
                    if (indegrees.get(next) == 0)
                        queue.offer(next);
                }
            }

            // 检查res长度是否==总字符数
            if (res.length() == indegrees.size())
                return res.toString();
            else
                return "";
        }

        /**
         * 返回 word1 word2 在该顺序下，体现的两字符的大小关系
         * 若 无法体现字符之间的大小关系，如 ab abc 则返回null
         * 若 word 之间出现了不可能的大小顺序，如 abc ab，标志位 valid = false
         * 
         * @param word1
         * @param word2
         * @return
         */
        private char[] getOrder(String word1, String word2) {
            for (int i = 0; i < Math.min(word1.length(), word2.length()); i++)
                if (word1.charAt(i) != word2.charAt(i))
                    return new char[] { word1.charAt(i), word2.charAt(i) };

            if (word1.length() > word2.length())
                valid = false;
            return null;
        }
    }

    // 277.搜寻名人
    // 假设你是一个专业的狗仔，参加了一个 n 人派对，其中每个人被从 0 到 n - 1 标号。
    // 在这个派对人群当中可能存在一位 “名人”。
    // 所谓 “名人” 的定义是：其他所有 n - 1 个人都认识他/她，而他/她并不认识其他任何人。
    // 现在你想要确认这个 “名人” 是谁，或者确定这里没有 “名人”。
    // 而你唯一能做的就是问诸如 “ A 你好呀，请问你认不认识 B 呀？” 的问题，以确定 A 是否认识 B。
    // 你需要在（渐近意义上）尽可能少的问题内来确定这位 “名人” 是谁（或者确定这里没有 “名人”）。
    // 在本题中，你可以使用辅助函数 bool knows(a, b) 获取到 A 是否认识 B。请你来实现一个函数 int findCelebrity(n)。
    // 派对最多只会有一个 “名人” 参加。
    // 若 “名人” 存在，请返回他/她的编号；若 “名人” 不存在，请返回 -1。

    // 方法一：假设 + 验证 -时间复杂度：O(n)，空间复杂度：O(1)
    // 利用条件：派对最多只会有一个 “名人” 参加。
    public int findCelebrity(int n) {
        int celebrity = 0; // 先假定 0 就是名人
        for (int i = 1; i < n; i++) {
            // knows(celebrity, i) 为 false，celebrity 不认识 i，celebrity 可能是名人
            // knows(celebrity, i) 为 true，celebrity 认识 i，celebrity 肯定不是名人，i 可能是名人
            if (knows(celebrity, i))
                celebrity = i;
        }
        // 检验一下这个 celebrity 是否真的是名人
        for (int i = 0; i < n; i++) {
            if (celebrity == i)
                continue;

            // 所谓 “名人” 的定义是：其他所有 n - 1 个人都认识他/她，而他/她并不认识其他任何人。
            if (knows(celebrity, i) || !knows(i, celebrity))
                return -1;
        }
        return celebrity;
    }

    // 辅助函数 bool knows(a, b) 获取到 A 是否认识 B
    boolean knows(int a, int b) {
        return true;
    }

    // 方法二：（自己想的）先n^2遍历，i 是否认识 j，如果认识则排除是名人的可能性；
    // 最后再遍历确认是否是名人，即：不认识任何人的人是否被其他所有人认识
    // 时间复杂度：O(n^2)，空间复杂度：O(n^2)

    // 279.完全平方数
    // 给定正整数 n，找到若干个完全平方数（比如 1, 4, 9, 16, ...）使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
    // 给你一个整数 n ，返回和为 n 的完全平方数的 最少数量 。
    // 完全平方数 是一个整数，其值等于另一个整数的平方；换句话说，其值等于一个整数自乘的积。例如，1、4、9 和 16 都是完全平方数，而 3 和 11 不是。
    // 提示：
    // 1 <= n <= 104

    // 方法一：动态规划-时间复杂度：O(n√n)，空间复杂度：O(n)
    int maxInput = 1;
    int[] f = new int[10000];
    {
        f[1] = 1;
    }

    public int numSquares(int n) {
        if (n <= maxInput)
            return f[n];

        for (int i = maxInput; i <= n; i++) {// 改进版，不用每次重复从1开始算起
            int minn = Integer.MAX_VALUE;
            for (int j = 1; j * j <= i; j++)// 核心：只用枚举√n个完全平方数
                minn = Math.min(minn, f[i - j * j]);
            f[i] = minn + 1;
        }
        maxInput = n;
        return f[n];
    }

    // 方法二：数学-时间复杂度：O(√n)，空间复杂度：O(1)
    // 一个数学定理可以帮助解决本题: 「四平方和定理」。
    // 四平方和定理证明了任意一个正整数都可以被表示为至多 四个正整数的平方和。这给出了本题的答案的上界。
    // 同时四平方和定理包含了一个更强的结论:
    // 当且仅当n ≠ 4k x (8m+7)时，n可以被表示为至多三个正整数的平方和。
    // 因此，当n=4k x (8m+7)时，n只能被表示为四个正整数的平方和。此时我们可以直接返回4。
    // 当n≠4k x (8m+7)时，我们需要判断到底多少个完全平方数能够表示n,我们知道答案只会是1,2,3中的一个:
    // 1.答案为1时，则必有n为完全平方数，这很好判断;
    // 2.答案为2时，则有n=a2+b2,我们只需要枚举所有的a(1≤a≤√n), 判断n - a2是否为完全平方
    // 数即可;
    // 3.答案为3时，我们很难在一个优秀的时间复杂度内解决它，但我们只需要检查答案为1或2的两种情
    // 况，即可利用排除法确定答案。
    public int numSquares2(int n) {
        if (isPerfectSquare(n)) // 判断是否为完全平方数
            return 1;

        if (checkAnswer4(n)) // 判断是否能表示为 4^k*(8m+7)
            return 4;

        for (int i = 1; i * i <= n; i++) {
            int j = n - i * i;
            if (isPerfectSquare(j))
                return 2;

        }
        return 3;
    }

    // 判断是否为完全平方数
    public boolean isPerfectSquare(int x) {
        int y = (int) Math.sqrt(x);
        return y * y == x;
    }

    // 判断是否能表示为 4^k*(8m+7)
    public boolean checkAnswer4(int x) {
        while (x % 4 == 0)
            x /= 4;

        return x % 8 == 7;
    }

    // 283.移动零
    // 给定一个数组 nums，编写一个函数将所有 0 移动到数组的末尾，同时保持非零元素的相对顺序。
    // 说明:
    // 必须在原数组上操作，不能拷贝额外的数组。
    // 尽量减少操作次数

    // 方法一：双指针（基于交换）-时间复杂度：O(n)，空间复杂度：O(1)
    // 左指针指向当前已经处理好的序列的尾部，右指针指向待处理序列的头部。
    class ssoltion_283 {
        public void moveZeroes(int[] nums) {
            int n = nums.length;
            int left = 0;// 期望指向0
            int right = 0;// 交换时指向数字
            while (right < n) {
                if (nums[right] != 0) {
                    swap(nums, left, right);
                    left++;
                    right++;
                } else
                    right++;
            }
        }

        public void swap(int[] nums, int i, int j) {
            int temp = nums[i];
            nums[i] = nums[j];
            nums[j] = temp;
        }
    }

    // 方法二：自己写的lowb找规律（基于移动）-时间复杂度：O(n)，空间复杂度：O(1)
    public void moveZeroes2(int[] nums) {
        int n = nums.length;
        int zeroCount = 0;
        // 每次移动非0元素即达到其最终位置
        for (int i = 0; i < n; i++) {
            if (nums[i] == 0)
                zeroCount += 1;
            else
                nums[i - zeroCount] = nums[i];
        }
        // 末尾置0
        for (int j = n - zeroCount; j < n; j++)
            nums[j] = 0;
    }

    // 285.二叉搜索树中的中序后继
    // 给定一棵二叉搜索树和其中的一个节点 p ，找到该节点在树中的中序后继。如果节点没有中序后继，请返回 null 。
    // 节点 p 的后继是值比 p.val 大的节点中键值最小的节点，即按中序遍历的顺序节点 p 的下一个节点。

    // 方法一：利用二叉搜索树的性质-时空复杂度：O(logn)
    // 最后一跳进入右子树退出循环，中序后继是最后一次进入左子树的节点
    // 最后一跳进入左子树退出循环，中序后继是其父节点
    public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
        TreeNode cur = root;
        TreeNode result = null;
        while (cur != null) {
            if (cur.val > p.val) {// 当前节点值大于p，记录该节点（可能是p的后继），进入左子树（寻找尽可能小的后继节点）
                result = cur;
                cur = cur.left;
            } else// 当前节点值小于等于p，进入右子树（寻找更大的节点）
                cur = cur.right;
        }
        return result;
    }

    // 方法二：dfs 递归（自己写的，时间好像也差不多）-时空复杂度：O(n)
    // 不是二叉搜索树也行
    class ssoltion_285 {
        boolean findP = false;
        TreeNode res;

        public TreeNode inorderSuccessor2(TreeNode root, TreeNode p) {
            dfs(root, p);
            return res;
        }

        public void dfs(TreeNode root, TreeNode p) {
            if (root == null)
                return;

            dfs(root.left, p);
            if (findP) {// 找到p后的第一个节点即为中序后继
                res = root;
                findP = false;// 防止记录后面节点
            }
            if (root == p)
                findP = true;
            dfs(root.right, p);
        }
    }

    // 287.寻找重复数
    // 给定一个包含 n + 1 个整数的数组 nums ，其数字都在 1 到 n 之间（包括 1 和 n），可知至少存在一个重复的整数
    // 假设 nums 只有 一个重复的整数 ，找出 这个重复的数
    // nums 中 只有一个整数 出现 两次或多次 ，其余整数均只出现 一次
    // nums.length == n + 1
    // 1 <= nums[i] <= n
    // 你设计的解决方案必须不修改数组 nums 且只用常量级 O(1) 的额外空间，即空间复杂度O(1)

    // 方法一：二进制-时间复杂度O(nlogn)
    // 正确性证明：
    // 数组中 target 出现了两次，则每个整数都有且仅有出现一次
    // 数组中 target 出现了三次及以上，那么必然有一些数不在 nums 数组中了，相当于用 target 去替换了这些数
    // 考虑替换的时候对 x 的影响，满足x > y 置为1（精髓所在）
    int findDuplicate(int[] nums) {
        int len = nums.length;// 对应n+1个整数
        int ans = 0;
        int bit_max = 31;// 对应数组长度所需的最大幂-1，整数的最大幂-1=31
        // 确定最高位，减少计算量（数组元素上界 = n）
        while (((len - 1) >> bit_max) == 0)
            bit_max -= 1;

        // 依次统计二进制数（从右往左数）的第bit位
        for (int bit = 0; bit <= bit_max; ++bit) {
            int x = 0;// 数组中所有数的第bit位之和
            int y = 0;// 0-n的第bit位之和
            for (int i = 0; i < len; ++i) {
                if ((nums[i] & (1 << bit)) != 0)// 第bit位为1
                    x += 1;
                if (i >= 1 && ((i & (1 << bit)) != 0))// 0到n的第bit位
                    y += 1;
            }

            if (x > y)// x > y时，第bit位置为1
                ans |= 1 << bit;// 或运算
        }
        return ans;
    }

    // 方法二：二分查找（左右边界不是数组的首尾元素，而是为了确定target的左右边界）-时间复杂度O(nlogn)
    // 定义 cnt[i] 表示 nums 数组中小于等于 i 的数有多少个
    // 假设重复的数是 target，那么 [1,target−1] 里的所有数满足cnt[i] ≤ i
    // [target,n] 里的所有数满足cnt[i] > i，具有单调性
    int findDuplicate2(int[] nums) {
        int len = nums.length;// 对应n+1个整数
        int l = 1, r = len - 1;// 左右指针
        int ans = -1;
        while (l <= r) {// 二分查找 logn趟
            int mid = (l + r) / 2;
            int cnt = 0;
            for (int i = 0; i < len; ++i)
                if (nums[i] <= mid)
                    cnt++;
            // [1,target−1] [target,n]
            if (cnt <= mid)// 则位于左半区间
                l = mid + 1;
            else {// 位于右半区间
                r = mid - 1;
                ans = mid;// 只有右半区间才包含正确的ans值，不管最后是从左半区间还是右半区间退出循环，ans值最后一次更新都必须来自右半区间
            }
        }
        return ans;
    }

    // 方法三：快慢指针-时间复杂度O(n)
    // 「Floyd 判圈算法」（又称龟兔赛跑算法），它是一个检测链表是否有环的算法
    // 对 nums 数组建图，每个位置 i 连一条 i→nums[i] 的边
    // 由于存在的重复的数字 target，因此 target 这个位置一定有起码两条指向它的边
    // 因此整张图一定存在环，且我们要找到的 target 就是这个环的入口
    // 先设置慢指针 slow 和快指针 fast ，慢指针每次走一步，快指针每次走两步
    // 根据「Floyd判圈算法」，两个指针在有环的情况下一定会相遇，
    // 此时再将 slow 放置起点 0，两个指针每次同时移动一步，相遇的点就是答案
    int findDuplicate3(int[] nums) {
        int slow = 0, fast = 0;// 快慢指针，指向nums对应下标的值
        do {// 1 ≤ nums[i] ≤ n，而nums长度为n+1，索引值为[0,n]，指针值为下一步的索引值
            slow = nums[slow];// 每次走一步
            fast = nums[nums[fast]];// 每次走两步
        } while (slow != fast);// 值相等跳出，此时有可能停在同一个值上，也可能停在ans值对应的两个位置上
        slow = 0;
        while (slow != fast) {
            slow = nums[slow];
            fast = nums[fast];
        }
        return slow;
    }

    // 289.生命游戏
    // 根据 百度百科 ， 生命游戏 ，简称为 生命 ，是英国数学家约翰·何顿·康威在 1970 年发明的细胞自动机。
    // 给定一个包含 m × n 个格子的面板，每一个格子都可以看成是一个细胞。
    // 每个细胞都具有一个初始状态： 1 即为 活细胞 （live），或 0 即为 死细胞 （dead）。
    // 每个细胞与其八个相邻位置（水平，垂直，对角线）的细胞都遵循以下四条生存定律：
    // 如果活细胞周围八个位置的活细胞数少于两个，则该位置活细胞死亡；
    // 如果活细胞周围八个位置有两个或三个活细胞，则该位置活细胞仍然存活；
    // 如果活细胞周围八个位置有超过三个活细胞，则该位置活细胞死亡；
    // 如果死细胞周围正好有三个活细胞，则该位置死细胞复活；
    // 下一个状态是通过将上述规则同时应用于当前状态下的每个细胞所形成的，其中细胞的出生和死亡是同时发生的。
    // 给你 m x n 网格面板 board 的当前状态，返回下一个状态。
    // 提示：
    // m == board.length
    // n == board[i].length
    // 1 <= m, n <= 25
    // board[i][j] 为 0 或 1
    // 进阶：
    // 你可以使用原地算法解决本题吗？请注意，面板上所有格子需要同时被更新：你不能先更新某些格子，然后使用它们的更新后的值再更新其他格子。
    // 本题中，我们使用二维数组来表示面板。原则上，面板是无限的，但当活细胞侵占了面板边界时会造成问题。你将如何解决这些问题？

    // 方法一：复制原数组进行模拟-时间复杂度：O(mn)，空间复杂度：O(mn)

    // 方法二：（自己写的）使用额外的状态-时间复杂度：O(mn)，空间复杂度：O(1)
    // 感觉官方题解定义的额外的状态不如自己的直观
    public void gameOfLife(int[][] board) {
        int m = board.length;
        int n = board[0].length;
        int[][] directions = new int[][] { { -1, -1 }, { -1, 0 }, { -1, 1 }, { 0, -1 }, { 0, 1 }, { 1, -1 }, { 1, 0 },
                { 1, 1 } };
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                int liveNum = 0;
                for (int[] direction : directions) {
                    int newi = i + direction[0];
                    int newj = j + direction[1];
                    if (0 <= newi && newi < m && 0 <= newj && newj < n && board[newi][newj] % 2 == 1)
                        liveNum++;
                }

                if (board[i][j] == 1) {// 活→
                    if (liveNum < 2 || liveNum > 3) // →死
                        board[i][j] += 2;
                } else {// 死→
                    if (liveNum == 3)// →活
                        board[i][j] += 2;
                }
            }
        }

        // 最后再统一修改
        for (int i = 0; i < m; i++)
            for (int j = 0; j < n; j++)
                if (board[i][j] > 1)// 状态产生变化
                    board[i][j] = 1 - board[i][j] % 2;
    }

    // 295.数据流的中位数
    // 中位数是有序整数列表中的中间值。如果列表的大小是偶数，则没有中间值，中位数是两个中间值的平均值。
    // 例如 arr = [2,3,4] 的中位数是 3 。
    // 例如 arr = [2,3] 的中位数是 (2 + 3) / 2 = 2.5 。
    // 实现 MedianFinder 类:
    // MedianFinder() 初始化 MedianFinder 对象。
    // void addNum(int num) 将数据流中的整数 num 添加到数据结构中。
    // double findMedian() 返回到目前为止所有元素的中位数。与实际答案相差 10-5 以内的答案将被接受。
    // 提示:
    // -105 <= num <= 105
    // 在调用 findMedian 之前，数据结构中至少有一个元素
    // 最多 5 * 104 次调用 addNum 和 findMedian

    // 方法一：优先队列-时间复杂度：addNum: O(logn)，findMedian: O(1)。空间复杂度：O(n)
    // 思路和算法
    // 我们用两个优先队列 queMax（小根堆）和 queMin（大根堆）分别记录大于中位数的数和小于等于中位数的数。
    // 即：两个堆中元素的数量关系要满足：queMin = [queMax, queMax + 1]
    // 当累计添加的数的数量为奇数时，queMin 中的数的数量比 queMax 多一个，此时中位数为 queMin 的队头。
    // 当累计添加的数的数量为偶数时，两个优先队列中的数的数量相同，此时中位数为它们的队头的平均值。

    // 当我们尝试添加一个数 num 到数据结构中，我们需要分情况讨论：
    // 1. num≤max{queMin}
    // num 小于等于中位数，我们需要将该数添加到 queMin 中。
    // 新的中位数将小于等于原来的中位数，因此我们可能需要将 queMin 中最大的数移动到 queMax 中。
    // 2. num>max{queMin}
    // 此时 num 大于中位数，我们需要将该数添加到 queMax 中。
    // 新的中位数将大于等于原来的中位数，因此我们可能需要将 queMax 中最小的数移动到 queMin 中。
    // 特别地，当累计添加的数的数量为 0 时，我们将 num 添加到 queMin 中。
    class MedianFinder {
        PriorityQueue<Integer> queMin;
        PriorityQueue<Integer> queMax;

        public MedianFinder() {
            queMin = new PriorityQueue<Integer>((a, b) -> (b - a));// 大根堆
            queMax = new PriorityQueue<Integer>((a, b) -> (a - b));// 小根堆
        }

        // 两个堆中元素的数量关系要满足：queMin = [queMax, queMax + 1]
        public void addNum(int num) {
            if (queMin.isEmpty() || num <= queMin.peek()) {
                queMin.offer(num);
                if (queMin.size() > queMax.size() + 1)// queMin中元素太多了，匀一个给queMax
                    queMax.offer(queMin.poll());
            } else {
                queMax.offer(num);
                if (queMin.size() < queMax.size())// queMin中元素太少了，匀一个给queMin
                    queMin.offer(queMax.poll());
            }
        }

        public double findMedian() {
            if (queMin.size() > queMax.size())// 奇数个元素
                return queMin.peek();
            return (queMin.peek() + queMax.peek()) / 2.0;// 偶数个元素
        }
    }

    // 方法一：（自己写的）优先队列-时间复杂度：addNum: O(logn)，findMedian: O(1)。空间复杂度：O(n)
    // 通过分情况讨论，降低了堆操作次数的平均值，执行时间稍短
    class MedianFinder11 {
        PriorityQueue<Integer> smaller;
        PriorityQueue<Integer> bigger;

        public MedianFinder11() {
            this.smaller = new PriorityQueue<>((x, y) -> y - x);// 大根堆，只关心更小部分数的最大值
            this.bigger = new PriorityQueue<>((x, y) -> x - y);// 小根堆，只关心更大部分数的最小值
        }

        public void addNum(int num) {
            // 1. 两堆大小相等，放小堆里
            if (smaller.size() == bigger.size()) {
                if (smaller.isEmpty() || num < bigger.peek())// 小堆为空（此时大堆必为空），当前数比大堆最小值还小（直接放入小堆即可）
                    smaller.offer(num);
                else {// 当前数比大堆最小值还大，需要交换放入的值
                    smaller.offer(bigger.poll());
                    bigger.offer(num);
                }
            }
            // 2. 小堆比大堆数量多，放大堆里
            else {
                if (num > smaller.peek())// 此时小堆必有值），当前数比小堆最大值还大（直接放入大堆即可）
                    bigger.offer(num);
                else {// 当前数比小堆最大值还小，需要交换放入的值
                    bigger.offer(smaller.poll());
                    smaller.offer(num);
                }
            }
        }

        public double findMedian() {
            if (smaller.size() == bigger.size())
                return ((double) smaller.peek() + bigger.peek()) / 2;
            else
                return (double) smaller.peek();
        }
    }

    // 297.二叉树的序列化与反序列化
    // 序列化是将一个数据结构或者对象转换为连续的比特位的操作，进而可以将转换后的数据存储在一个文件或者内存中
    // 同时也可以通过网络传输到另一个计算机环境，采取相反方式重构得到原数据
    // 请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列（自己设计序列）
    // 反序列化算法执行逻辑，你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构
    // 树中节点数在范围 [0, 104] 内
    // -1000 <= Node.val <= 1000
    // 题目要求：
    // 编解码器对象将被实例化并按此方式调用
    // Codec ser = new Codec();
    // Codec deser = new Codec();
    // TreeNode ans = deser.deserialize(ser.serialize(root));

    // 方法一：DFS（先序遍历）-时间复杂度、空间复杂度：O(n)
    // 先序遍历，String用,隔开，null用None表示，再按先序遍历反序列化
    // 含有所有null的先序遍历，是可以还原出树的结构的
    // String类型是引用数据类型，但是因为有字符串常量池，做参数是值的复制
    // StringBuilder是引用数据类型，做参数是传递地址
    String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder("");
        rserialize(root, sb);
        return new String(sb);
    }

    void rserialize(TreeNode root, StringBuilder str) {
        if (root == null)
            str.append("None,");
        else {
            str.append(String.valueOf(root.val) + ",");// 先序遍历
            rserialize(root.left, str);
            rserialize(root.right, str);
        }

    }

    TreeNode deserialize(String data) {
        String[] dataArray = data.split(",");
        Queue<String> dataList = new LinkedList<>(Arrays.asList(dataArray));// 数组转List，如果就要使用数组，需要传入index指向当前节点位置
        return rdeserialize(dataList);
    }

    TreeNode rdeserialize(Queue<String> dataList) {
        if ("None".equals(dataList.peek())) {
            dataList.poll();
            return null;
        }
        // 按先序遍历反序列化
        TreeNode root = new TreeNode(Integer.parseInt(dataList.peek()));
        dataList.poll();
        root.left = rdeserialize(dataList);
        root.right = rdeserialize(dataList);
        return root;
    }

    // 方法二：（自己写的）bfs-时间复杂度、空间复杂度：O(n)
    public String serialize2(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        // 辅助队列实现bfs遍历
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (node == null)
                sb.append("null,");
            else {
                sb.append(node.val);
                sb.append(",");
                queue.offer(node.left);
                queue.offer(node.right);
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }

    public TreeNode deserialize2(String data) {
        Queue<String> datas = new LinkedList<>(Arrays.asList(data.split(",")));
        // 辅助队列queue根据队列datas重建二叉树
        Queue<TreeNode> queue = new LinkedList<>();
        TreeNode root = stringToTreeNode(datas.poll());
        queue.offer(root);
        while (!queue.isEmpty()) {
            // 对于每一个节点，出队时找到其左右孩子，左右孩子入队
            TreeNode node = queue.poll();
            if (node != null) {
                TreeNode left = stringToTreeNode(datas.poll());
                TreeNode right = stringToTreeNode(datas.poll());
                node.left = left;
                node.right = right;
                queue.offer(left);
                queue.offer(right);
            }
        }
        return root;
    }

    // String转TreeNode
    private TreeNode stringToTreeNode(String str) {
        if ("null".equals(str))
            return null;
        else
            return new TreeNode(Integer.valueOf(str));
    }

    // 方法三：括号表示编码（后序遍历，String是中序遍历顺序）+ 递归下降解码
    // 时间复杂度、空间复杂度：O(n) 为什么时间缩短10倍
    // 也可以这样表示一颗二叉树：
    // 如果当前的树为空，则表示为 X
    // 如果当前的树不为空，则表示为 (左子树序列化之后的结果)当前节点val(右子树序列化之后的结果)
    // 根据这样的定义，很好写出序列化的过程，「后序遍历」这颗二叉树即可，像方法1那样从左至右序列化不行
    // 那如何反序列化呢？根据定义，我们可以推导出这样的巴科斯范式（BNF）：
    // T -> (T) num (T) | X
    // 它的意义是：用 T 代表一棵树序列化之后的结果，| 表示 T 的构成为 (T) num (T) 或者 X
    // | 左边是对 T的递归定义，右边规定了递归终止的边界条件
    String serialize3(TreeNode root) {
        return rserialize3(root);
    }

    String rserialize3(TreeNode root) {
        if (root == null)
            return "X";

        return "(" + rserialize3(root.left) + ")" + root.val + "(" + rserialize3(root.right) + ")";
    }

    TreeNode deserialize3(String data) {
        int[] ptr = { 0 };// 为了不创建类的成员变量，（基本数据类型传入数据的复制，引用数据类型传入地址），用包装类也不行
        return parse(data, ptr);
    }

    TreeNode parse(String data, int[] ptr) {
        if (data.charAt(ptr[0]) == 'X') {
            ++ptr[0];
            return null;
        }
        TreeNode cur = new TreeNode(0);
        ++ptr[0]; // 跳过左括号
        cur.left = parse(data, ptr);// 左子树
        ++ptr[0]; // 跳过右括号

        cur.val = parseInt(data, ptr);// 当前节点值

        ++ptr[0]; // 跳过左括号
        cur.right = parse(data, ptr);// 右子树
        ++ptr[0]; // 跳过右括号
        return cur;
    }

    int parseInt(String data, int[] ptr) {
        int x = 0, sgn = 1;// 节点值有正负，-1000 <= Node.val <= 1000
        if (!Character.isDigit(data.charAt(ptr[0]))) {// 数字第一位是-
            sgn = -1;
            ++ptr[0];
        }
        while (Character.isDigit(data.charAt(ptr[0])))
            x = x * 10 + data.charAt(ptr[0]++) - '0';

        return x * sgn;
    }

    // 300.最长递增子序列
    // 给你一个整数数组 nums ，找到其中最长严格递增子序列的长度。
    // 子序列是由数组派生而来的序列，删除（或不删除）数组中的元素而不改变其余元素的顺序。
    // 例如，[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
    // 提示：
    // 1 <= nums.length <= 2500
    // -104 <= nums[i] <= 104
    // 进阶：
    // 你可以设计时间复杂度为 O(n2) 的解决方案吗？
    // 你能将算法的时间复杂度降低到 O(n log(n)) 吗?

    // 方法一：动态规划-时间复杂度：O(n^2)，空间复杂度：O(n)
    // dp数组：索引为第 i 个数字做结尾，值为第 i 个数字结尾的最长上升子序列的长度
    // 值不具有单调性，只能挨个遍历，找到结尾小于当前数时，最大的个数
    // 定义 dp[i] 为考虑前 i 个元素，以第 i 个数字结尾的最长上升子序列的长度
    // 状态转移方程为：
    // dp[i] = max(dp[j]) + 1,其中0 ≤ j < i且 num[j] < num[i]
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0)
            return 0;

        int[] dp = new int[nums.length];
        dp[0] = 1;
        int maxans = 1;
        for (int i = 1; i < nums.length; i++) {// 以第i个数字为结尾
            dp[i] = 1; // 如果i小于前面所有数，则dp[i]为1
            for (int j = 0; j < i; j++)// 从头开始
                if (nums[j] < nums[i])
                    dp[i] = Math.max(dp[i], dp[j] + 1);// 长度+1

            maxans = Math.max(maxans, dp[i]);
        }
        return maxans;
    }

    // 方法二：贪心 + 二分查找-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // dp数组：索引为以数字结尾的最长上升子序列的长度，值为该长度下的最小值
    // 此时值必单调，可以使用二分查找加快查找速度，找到合适的序列进行追加
    // 考虑一个简单的贪心，如果我们要使上升子序列尽可能的长，则我们需要让序列上升得尽可能慢，
    // 因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
    // 维护一个数组 d[i] ，表示长度为 i 的最长上升子序列的末尾元素的最小值，且数组d具有单调性（有单调性才能二分）
    public int lengthOfLIS2(int[] nums) {
        int length = 1;// length 记录目前最长上升子序列的长度
        int n = nums.length;

        int[] d = new int[n + 1];// 长度为 i 的最长上升子序列的末尾元素的最小值
        d[length] = nums[0];
        for (int i = 1; i < n; ++i) {
            if (nums[i] > d[length])// 当前值可使长度加1，更新d数组（其实无需单独判断这一步）
                d[++length] = nums[i];
            else {// 当前值无法使长度加1，更新d数组（一定能更新，值一样就覆盖）
                int l = 1, r = length;
                // 找到第一个比 nums[i] 小的数 d[k]
                while (l <= r) {
                    int mid = (l + r) >> 1;
                    if (d[mid] < nums[i])
                        l = mid + 1;
                    else
                        r = mid - 1;
                }
                d[l] = nums[i];
            }
        }
        return length;
    }

    // 方法二：贪心 + 二分查找（javaAPI）-时间复杂度：O(nlogn)，空间复杂度：O(n)
    public int lengthOfLIS22(int[] nums) {
        int n = nums.length;// length 记录目前最长上升子序列的长度
        int dp[] = new int[n + 1];// 长度为 i 的最长上升子序列的末尾元素的最小值
        int length = 1;
        dp[length] = nums[0];
        for (int i = 1; i < n; i++) {
            int numi = nums[i];
            int numj = dp[length];
            if (numi > numj) {// 当前值可使长度加1，更新d数组（其实无需单独判断这一步）
                length++;
                dp[length] = numi;
            } else {// 当前值无法使长度加1，更新d数组（index为负表示插入位置，更新；为正表示找到相同值，不用更新）
                int index = Arrays.binarySearch(dp, 1, length + 1, numi);
                if (index < 0) {
                    index = -index - 1;
                    dp[index] = numi;
                }
            }
        }
        return length;
    }

    // 方法二：（自己写的）贪心 + 二分查找（javaAPI）-时间复杂度：O(nlogn)，空间复杂度：O(n)
    public int lengthOfLIS222(int[] nums) {
        int res = 1;
        int n = nums.length;

        int[] dp = new int[n + 1]; // 索引为长度，值为结尾最小元素
        // 预处理，方便后续二分查找
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = Integer.MIN_VALUE;

        for (int i = 0; i < n; i++) {
            int num = nums[i];
            int index = Arrays.binarySearch(dp, 0, res + 1, num);
            // index为正时，存在相同值为结尾的序列，不用更新
            // index为负时，转化为应当插入的位置，即以该值为结尾序列的长度，一定能更新（二分查找递增序列）
            if (index < 0) {
                index = -index - 1;
                dp[index] = num;
                res = Math.max(res, index);
            }
        }
        return res;
    }

    // 308.二维区域和检索-可变
    // 给你一个二维矩阵 matrix ，你需要处理下面两种类型的若干次查询：
    // 更新：更新 matrix 中某个单元的值。
    // 求和：计算矩阵 matrix 中某一矩形区域元素的 和 ，该区域由 左上角 (row1, col1) 和 右下角 (row2, col2) 界定。
    // 实现 NumMatrix 类：
    // NumMatrix(int[][] matrix) 用整数矩阵 matrix 初始化对象。
    // void update(int row, int col, int val)
    // 更新 matrix[row][col] 的值到 val 。
    // int sumRegion(int row1, int col1, int row2, int col2)
    // 返回矩阵 matrix 中指定矩形区域元素的 和 ，该区域由 左上角 (row1, col1) 和 右下角 (row2, col2) 界定。
    // 提示：
    // m == matrix.length
    // n == matrix[i].length
    // 1 <= m, n <= 200
    // -10^5 <= matrix[i][j] <= 10^5
    // 0 <= row < m
    // 0 <= col < n
    // -10^5 <= val <= 10^5
    // 0 <= row1 <= row2 < m
    // 0 <= col1 <= col2 < n
    // 最多调用10^4 次 sumRegion 和 update 方法

    // 方法一：哈希 + 惰性更新
    class NumMatrix {
        int[][] arr, pre;// 原始数组，前缀和数组
        int m, n;
        boolean hasChange;

        public NumMatrix(int[][] matrix) {
            arr = matrix;
            m = matrix.length;
            n = matrix[0].length;

            // 初始化前缀和数组
            pre = new int[m + 1][n + 1];
            for (int i = 0; i < m; i++)
                for (int j = 0; j < n; j++)
                    pre[i + 1][j + 1] = matrix[i][j] + pre[i][j + 1] + pre[i + 1][j] - pre[i][j];
        }

        public void update(int row, int col, int val) {
            arr[row][col] = val;
            hasChange = true;
        }

        public int sumRegion(int row1, int col1, int row2, int col2) {
            if (hasChange) {
                for (int i = 0; i < m; i++)
                    for (int j = 0; j < n; j++)
                        pre[i + 1][j + 1] = arr[i][j] + pre[i][j + 1] + pre[i + 1][j] - pre[i][j];
                hasChange = false;
            }
            return pre[row2 + 1][col2 + 1] + pre[row1][col1] - pre[row1][col2 + 1] - pre[row2 + 1][col1];
        }
    }

    // 方法二：线段树
    // 二维线段树，记录左上角和右下角，共四个点
    // 1. 建立线段树，共四个点，一个和，两个孩子
    // 2. 递归分配孩子时，优先分行，再分列（方便，一次分四个也可以，需要判断的会多一点）
    // 3. 算出孩子的值，逐一往上递推，拿到整体前缀和
    // 4. 添加元素：不在范围内返回，在范围内递推添加，传递到孩子
    // 5. 求和： 当前范围值，都在目标范围内直接返回sum；都不在目标内直接返回0；否则累计孩子的和

    class NumMatrix2 {
        int[][] arr;
        SquareTree tree;

        public NumMatrix2(int[][] matrix) {
            arr = matrix;
            tree = new SquareTree(0, 0, matrix.length - 1, matrix[0].length - 1, matrix);
        }

        public void update(int row, int col, int val) {
            arr[row][col] = val;
            tree.add(row, col, val - arr[row][col]);
        }

        public int sumRegion(int row1, int col1, int row2, int col2) {
            return tree.getSum(row1, col1, row2, col2);
        }
    }

    // 线段树
    class SquareTree {
        int x1, x2, y1, y2, sum;// xy轴边界，当前区域内的和
        SquareTree t1, t2;// 子线段树，按一定规则划分当前线段树（优先分行，再分列）

        // 初始化线段树
        public SquareTree(int x1, int y1, int x2, int y2, int[][] arr) {
            this.x1 = x1;
            this.y1 = y1;
            this.x2 = x2;
            this.y2 = y2;
            sum = initSum(x1, y1, x2, y2, arr);
        }

        // 递归求和，孩子分配好后，再由下往上求和
        private int initSum(int X1, int Y1, int X2, int Y2, int[][] arr) {
            if (X1 == X2 && Y1 == Y2) {
                sum = arr[X1][Y1];
                return sum;
            }

            // 分配孩子时，优先分行，再分列（方便，一次分四个也可以，需要判断的会多一点）
            if (x1 < x2) {// 分行
                int mid = x1 + (x2 - x1) / 2;
                t1 = new SquareTree(x1, y1, mid, y2, arr);
                t2 = new SquareTree(mid + 1, y1, x2, y2, arr);
            } else {// 此时x1==x2，分列
                int mid = y1 + (y2 - y1) / 2;
                t1 = new SquareTree(x1, y1, x2, mid, arr);
                t2 = new SquareTree(x1, mid + 1, x2, y2, arr);
            }

            sum = t1.sum + t2.sum;
            return sum;
        }

        // 修改数组后，修改线段树
        public void add(int x, int y, int v) {
            // 修改数值不在线段树范围内
            if (x < x1 || x > x2 || y < y1 || y > y2)
                return;

            sum += v;
            if (t1 != null)
                t1.add(x, y, v);
            if (t2 != null)
                t2.add(x, y, v);
        }

        // 查询某区域内的和
        // X1, X2, Y1, Y2 要查询的区域的边界
        // x1, x2, y1, y2 线段树的边界
        // 线段树的叶子节点是数组的单个元素，所以通过这种方式一定能够拼凑出要查询的区域内的和
        public int getSum(int X1, int Y1, int X2, int Y2) {
            // 完全不相交（要查询的区域，线段树区域）
            if (x2 < X1 || x1 > X2 || y2 < Y1 || y1 > Y2)
                return 0;

            // 包含，要查询的区域包含线段树区域，即线段树区域是要查询的区域内的一部分
            if (X1 <= x1 && X2 >= x2 && Y1 <= y1 && Y2 >= y2)
                return sum;

            // 相交，但此时线段树区域不完全是要查询的区域
            // 要查询的区域的一部分是线段树区域内的一部分，需要继续在子线段树中寻找
            return t1.getSum(X1, Y1, X2, Y2) + t2.getSum(X1, Y1, X2, Y2);
        }
    }

    // 315.计算右侧小于当前元素的个数
    // 给你一个整数数组 nums ，按要求返回一个新数组 counts 。
    // 数组 counts 有该性质： counts[i] 的值是  nums[i] 右侧小于 nums[i] 的元素的数量。
    // 提示：
    // 1 <= nums.length <= 105
    // -104 <= nums[i] <= 104

    // 方法一：离散化树状数组-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 「树状数组」是一种可以动态维护序列前缀和的数据结构，它的功能是：
    // 单点更新 update(i, v)： 把序列 i 位置的数加上一个值 v，在该题中 v = 1
    // 区间查询 query(i)： 查询序列 [1⋯i] 区间的区间和，即 i 位置的前缀和
    // 修改和查询的时间代价都是 O(logn)，其中 n 为需要维护前缀和的序列的长度。

    // 转化为动态维护前缀和问题
    // 记 value 序列为 v，我们可以看出它第 i - 1 位的前缀和表示「有多少个数比 i 小」。
    // 那么我们可以从后往前遍历序列 a，记当前遍历到的元素为 a_i，我们把 a_i 对应的桶的值自增 1，记 a_i = p，
    // 把 v 序列 p - 1 位置的前缀和加入到答案中算贡献。
    // 为什么这么做是对的呢，因为我们在循环的过程中，我们把原序列分成了两部分，后半部部分已经遍历过（已入桶），
    // 前半部分是待遍历的（未入桶），那么我们求到的 p - 1 位置的前缀和就是「已入桶」的元素中比 p 小的元素的个数总和。
    // 这种动态维护前缀和的问题我们可以用「树状数组」来解决。

    // 用离散化优化空间
    // 我们显然可以用数组来实现这个桶，可问题是如果 a_i 中有很大的元素，
    // 比如 10^9 ，我们就要开一个大小为 10^9 的桶，内存中是存不下的。
    // 这个桶数组中很多位置是 0，有效位置是稀疏的，我们要想一个办法让有效的位置全聚集到一起，
    // 减少无效位置的出现，这个时候我们就需要用到一个方法——离散化。
    // 离散化的方法有很多，但是目的是一样的，即把原序列的值域映射到一个连续的整数区间，并保证它们的偏序关系不变。
    // 这里我们将原数组去重后排序，原数组每个数映射到去重排序后这个数对应位置的下标，
    // 我们称这个下标为这个对应数字的 id。已知数字获取 id 可以在去重排序后的数组里面做二分查找，
    // 已知 id 获取数字可以直接把 id 作为下标访问去重排序数组的对应位置。

    class ssoltion_315_1 {
        private int[] c;// 动态的树状数组

        // 将nums数组中离散的数映射为连续的索引值
        // 长度为nums数组去重后个数，值为去重排序后的nums数组值
        private int[] a;

        public List<Integer> countSmaller(int[] nums) {
            List<Integer> resultList = new ArrayList<Integer>();
            discretization(nums);// 离散化，去重 + 排序
            init(nums.length);// 初始化树状数组c
            for (int i = nums.length - 1; i >= 0; --i) {
                int id = getId(nums[i]);// num 在 a 中的索引 + 1，方便后续树状数组的处理
                resultList.add(query(id - 1));// 计算前缀和
                update(id);// 更新树状数组
            }
            Collections.reverse(resultList);
            return resultList;
        }

        private void init(int length) {
            c = new int[length];
            Arrays.fill(c, 0);
        }

        private int lowBit(int x) {
            return x & (-x);
        }

        // 更新树状数组
        private void update(int pos) {
            while (pos < c.length) {
                c[pos] += 1;
                pos += lowBit(pos);
            }
        }

        // 计算前缀和
        private int query(int pos) {
            int ret = 0;
            while (pos > 0) {
                ret += c[pos];
                pos -= lowBit(pos);
            }
            return ret;
        }

        // 离散化，去重 + 排序
        private void discretization(int[] nums) {
            Set<Integer> set = new HashSet<Integer>();
            for (int num : nums)
                set.add(num);

            a = new int[set.size()];
            int index = 0;
            for (int num : set)
                a[index++] = num;
            Arrays.sort(a);
        }

        private int getId(int x) {
            return Arrays.binarySearch(a, x) + 1;
        }
    }

    // 方法二：归并排序-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 借鉴逆序对问题中的归并排序
    class ssolution_315_2 {
        // 记录每个数字对应的原数组中的下标，下标与原数组nums相对保持一致，值为原数组的下标
        private int[] index;

        private int[] temp;// 拷贝数组
        private int[] tempIndex;// 拷贝数组对应索引

        // 记录最终答案，下标与排序数组nums相对保持一致，值为答案
        private int[] ans;

        public List<Integer> countSmaller(int[] nums) {
            this.index = new int[nums.length];
            this.temp = new int[nums.length];
            this.tempIndex = new int[nums.length];
            this.ans = new int[nums.length];
            for (int i = 0; i < nums.length; ++i)
                index[i] = i;

            int l = 0, r = nums.length - 1;
            mergeSort(nums, l, r);
            List<Integer> list = new ArrayList<Integer>();
            for (int num : ans)
                list.add(num);

            return list;
        }

        public void mergeSort(int[] a, int l, int r) {
            // 问题的最小长度
            if (l >= r)
                return;

            int mid = (l + r) >> 1;
            // 问题分解
            mergeSort(a, l, mid);
            mergeSort(a, mid + 1, r);

            // 归并
            merge(a, l, mid, r);
        }

        public void merge(int[] a, int l, int mid, int r) {
            int i = l, j = mid + 1, p = l;// 左右子序列起始下标，拷贝数组下标
            while (i <= mid && j <= r) {
                // 左子序列更小，放入nums数组，并计算答案，数值相等时，优先放入左子序列中的数
                // 题目要求严格小于，这样做可以保证左右子序列同时有相同数字时正确计算答案
                if (a[i] <= a[j]) {
                    temp[p] = a[i];
                    tempIndex[p] = index[i];
                    ans[index[i]] += (j - mid - 1);
                    ++i;
                    ++p;
                } else {// 右子序列更小，放入nums数组
                    temp[p] = a[j];
                    tempIndex[p] = index[j];
                    ++j;
                    ++p;
                }
            }
            // 此时所有右子序列已放入nums数组中，并计算答案
            while (i <= mid) {
                temp[p] = a[i];
                tempIndex[p] = index[i];
                ans[index[i]] += (j - mid - 1);
                ++i;
                ++p;
            }
            // 此时所有左子序列已放入nums数组中，直接将子序列当前位置放入nums数组中即可
            while (j <= r) {
                temp[p] = a[j];
                tempIndex[p] = index[j];
                ++j;
                ++p;
            }

            // 把最终归并结果放入nums数组
            for (int k = l; k <= r; ++k) {
                index[k] = tempIndex[k];
                a[k] = temp[k];
            }
        }
    }

    // 方法三：（自己写的）动态维护有序数组 + 二分查找-时间复杂度：O(n^2)，空间复杂度：O(n)
    // 维护有序数组时是插入操作，导致实际时间复杂度仍然很高（是方法一方法二的20倍）
    public List<Integer> countSmaller3(int[] nums) {
        List<Integer> rightNums = new ArrayList<>();
        List<Integer> res = new ArrayList<>();
        for (int i = nums.length - 1; i >= 0; i--) {
            int num = nums[i];
            int left = 0, right = rightNums.size() - 1;
            // 查找第一个大于num-1的值的位置
            while (left <= right) {
                int mid = (left + right) / 2;
                if (rightNums.get(mid) <= num - 1)
                    left = mid + 1;
                else
                    right = mid - 1;
            }
            res.add(left);
            rightNums.add(left, num);
        }
        Collections.reverse(res);
        return res;
    }

    // 方法四：（自己写的）动态维护桶（多个有序数组） + 二分查找-时间复杂度：O(n^2)，空间复杂度：O(n)
    // 维护有序数组时是插入操作，导致实际时间复杂度仍然很高（是方法一方法二的10倍）
    // -104 <= nums[i] <= 104 映射到 [0, 2*10^4]
    public List<Integer> countSmaller4(int[] nums) {
        int bucketSize = 1000;
        int bucketNum = 20000 / bucketSize;
        List<List<Integer>> buckets = new ArrayList<>();
        for (int i = 0; i <= bucketNum; i++) {
            List<Integer> bucket = new ArrayList<>();
            buckets.add(bucket);
        }

        List<Integer> res = new ArrayList<>();
        for (int i = nums.length - 1; i >= 0; i--) {
            int num = nums[i];
            int bucketIndex = (num + 10000) / bucketSize;
            List<Integer> bucket = buckets.get(bucketIndex);
            int left = 0, right = bucket.size() - 1;
            // 查找第一个大于num-1的值的位置
            while (left <= right) {
                int mid = (left + right) / 2;
                if (bucket.get(mid) <= num - 1)
                    left = mid + 1;
                else
                    right = mid - 1;
            }
            bucket.add(left, num);
            int ans = left;
            for (int j = 0; j < bucketIndex; j++)
                ans += buckets.get(j).size();
            res.add(ans);
        }

        Collections.reverse(res);
        return res;
    }

    // 322.零钱兑换
    // 给你一个整数数组 coins ，表示不同面额的硬币；以及一个整数 amount ，表示总金额。
    // 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额，返回 -1 。
    // 你可以认为每种硬币的数量是无限的。
    // 提示：
    // 1 <= coins.length <= 12
    // 1 <= coins[i] <= 231 - 1 没有面值为0的币
    // 0 <= amount <= 104

    // 方法一：动态规划-时间复杂度：O(Sn)，其中 S 是金额，n 是面额数。
    // 我们一共需要计算 O(S) 个状态，S 为题目所给的总金额。对于每个状态，每次需要枚举 n 个面额来转移状态。
    // 空间复杂度：O(S)。
    // dp的下标表示要凑出面额
    // 先确定末尾（要凑出的面额）（防止数据溢出）
    public int coinChange(int[] coins, int amount) {

        int max = amount + 1;// 最终答案的最大值为amount（全部由面额1组成）
        int[] dp = new int[amount + 1];// dp的下标表示要凑出面额
        Arrays.fill(dp, max);// 初始化为amount + 1 方便后面比较取小，且只要最终答案的值大于max，一定无法凑出该面额
        dp[0] = 0;// 方便后续凑

        for (int i = 1; i <= amount; i++) // 期望凑到面额i，amount=0则直接跳出循环返回-1
            for (int coin : coins) // 尝试使用各种面额凑到面额i，面额i =（总额i-该面额）+ 该面额
                if (coin <= i)
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);

        return dp[amount] >= max ? -1 : dp[amount];
    }

    // 方法二：动态规划-时间复杂度：O(Sn)，空间复杂度：O(S)。
    // 先确定起始（从该面额开始凑）
    public int coinChange2(int[] coins, int amount) {

        int max = amount + 1;// 最终答案的最大值为amount（全部由面额1组成）
        int[] dp = new int[amount + 1];// dp的下标表示要凑出面额
        Arrays.fill(dp, max);// 初始化为amount + 1 方便后面比较取小，且只要最终答案的值大于max，一定无法凑出该面额
        dp[0] = 0;// 方便后续凑

        for (int i = 0; i <= amount; i++) // 从总额i开始凑，amount=0则直接跳出循环返回-1
            for (int coin : coins) // （总额i-该面额）+ 各面额
                if (i + (long) coin <= amount)// 样例有整型最大值的面额，醉了
                    dp[i + coin] = Math.min(dp[i + coin], dp[i] + 1);

        return dp[amount] >= max ? -1 : dp[amount];
    }

    // 方法三：dfs 记忆化搜索-时间复杂度：O(Sn)，其中 S 是金额，n 是面额数。空间复杂度：O(S)
    // 为了避免重复的计算，我们将每个子问题的答案存在一个数组中进行记忆化，如果下次还要计算这个问题的值直接从数组中取出返回即可，
    // 这样能保证每个子问题最多只被计算一次。
    public int coinChange3(int[] coins, int amount) {
        if (amount < 1)
            return 0;

        return coinChange(coins, amount, new int[amount]);
    }

    private int coinChange(int[] coins, int rem, int[] count) {
        if (rem < 0)
            return -1;

        if (rem == 0)
            return 0;

        if (count[rem - 1] != 0)// 通过数组避免一些重复计算，对于每个不同的问题（不同面额的币），count都不相同
            return count[rem - 1];

        int min = Integer.MAX_VALUE;
        for (int coin : coins) {
            int res = coinChange(coins, rem - coin, count); // 选择一种币，分解成几个子问题
            if (res >= 0 && res < min) // res=-1则表示无法凑出
                min = 1 + res;

        }
        count[rem - 1] = (min == Integer.MAX_VALUE) ? -1 : min;// 更新数组
        return count[rem - 1];
    }

    // 324.摆动排序II
    // 此题为「280. 摆动排序」的扩展题目，不同之处在于本题要求排序后的相邻的元素有严格的大小关系
    // 给你一个整数数组 nums，将它重新排列成 nums[0] < nums[1] > nums[2] < nums[3]... 的顺序。
    // 你可以假设所有输入数组都可以得到满足题目要求的结果。
    // 提示：
    // 1 <= nums.length <= 5 * 104
    // 0 <= nums[i] <= 5000
    // 题目数据保证，对于给定的输入 nums ，总能产生满足题目要求的结果
    // 进阶：你能用 O(n) 时间复杂度和 / 或原地 O(1) 额外空间来实现吗？

    // 方法一：排序-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 排序后将数组一分为二，由高到低依次放入较小，较大，...（解决较大子数组最小值等于较小子数组最大值时得到错误答案，同时按题意一定有解）
    public void wiggleSort(int[] nums) {
        int[] arr = nums.clone();
        Arrays.sort(arr);
        int n = nums.length;
        int x = (n + 1) / 2;// 将数组一分为二

        // i指向nums数组下标，j指向较大子数组末尾，k指向较小子数组末尾，依次放入较小，较大，...
        for (int i = 0, j = x - 1, k = n - 1; i < n; i += 2, j--, k--) {
            nums[i] = arr[j];
            if (i + 1 < n)
                nums[i + 1] = arr[k];
        }
    }

    // 方法二：三向切分-时间复杂度：O(n)，空间复杂度：O(n)，（感觉快排找中间值已经算nlogn了...）
    // 核心思路：
    // 1. 利用快排找到数组中间值
    // 2. 处理较大子数组最小值或较小子数组最大值，等于中间值的情况
    // 将数组分成三部分，依次为小于nums[mid] 的部分，等于 nums[mid] 的部分，大于 nums[mid] 的部分。
    // 使数组满足：nums[0,...,i-1]严格小于nums[mid]，nums[i,...,j]等于nums[mid]，nums[j+1,...,n-1]严格大于nums[mid]
    // 3. 按照顺序构建最终答案
    class ssolution_324_2 {
        Random random = new Random();

        public void wiggleSort(int[] nums) {
            // 1. 利用快排找到nums数组的中间值，此时数组整体不一定有序
            int n = nums.length;
            int x = (n + 1) / 2;
            int mid = x - 1;
            int target = findKthLargest(nums, n - mid);

            // 2. 处理较大子数组最小值或较小子数组最大值，等于中间值的情况（三指针）
            // 假设nums[0,...,i-1]严格小于nums[mid]，nums[i,...,j]等于nums[mid]，nums[j+1,...,n-1]严格大于nums[mid]
            // k指向nums数组起始位置，i指向较小子数组起始位置，j指向较大子数组末尾位置
            // 当满足 0≤k<i 时，nums[k]<nums[mid]；
            // 当满足 i≤k≤j 时，nums[k]=nums[mid]；
            // 当满足 j<k≤n−1 时，nums[mid]<nums[k]
            for (int k = 0, i = 0, j = n - 1; k <= j; k++) {
                // 当前数严格大于中间值
                if (nums[k] > target) {
                    // 此时k处于第一第二部分子数组，遍历第三部分子数组，从后往前，直到遍历到小于等于中间值
                    while (j > k && nums[j] > target)
                        j--;
                    swap(nums, k, j);
                    j--;
                }

                // 当前数等于中间值时不做处理，等待k进入第二部分子数组后交换

                // 当前数严格小于中间值
                // i始终指向第一部分中第一个不满足条件的数
                // k处于第一部分时无影响，k处于第二第三部分子数组时会将数交换到合适的位置
                if (nums[k] < target) {
                    swap(nums, k, i);
                    i++;
                }
            }

            // 3. 按照顺序构建最终答案
            int[] arr = nums.clone();
            // i指向nums数组下标，j指向较大子数组末尾，k指向较小子数组末尾，依次放入较小，较大，...
            for (int i = 0, j = x - 1, k = n - 1; i < n; i += 2, j--, k--) {
                nums[i] = arr[j];
                if (i + 1 < n)
                    nums[i + 1] = arr[k];
            }
        }

        public int findKthLargest(int[] nums, int k) {
            return quickSelect(nums, 0, nums.length - 1, nums.length - k);
        }

        public int quickSelect(int[] a, int l, int r, int index) {
            int q = randomPartition(a, l, r);
            if (q == index) {
                return a[q];
            } else {
                return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
            }
        }

        public int randomPartition(int[] a, int l, int r) {
            int i = random.nextInt(r - l + 1) + l;
            swap(a, i, r);
            return partition(a, l, r);
        }

        public int partition(int[] a, int l, int r) {
            int x = a[r], i = l - 1;
            for (int j = l; j < r; ++j) {
                if (a[j] <= x) {
                    swap(a, ++i, j);
                }
            }
            swap(a, i + 1, r);
            return i + 1;
        }

        public void swap(int[] a, int i, int j) {
            int temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
    }

    // 方法三：三向切分 + 索引转换-时间复杂度：O(n)，空间复杂度：O(logn)
    // 基于方法一、方法二的按照顺序构建最终答案的O(n)空间进行优化
    // 假设摆动排序后最终答案数组为:
    // nums[mid], nums[n-1], nums[mid-1],..., nums[1], nums[mid+2], nums[0],
    // nums[mid+1]
    // 按照顺序构建最终答案前的数组为:
    // nums[0], nums[1],..., nums[mid-1], nums[mid], nums[mid+1],..., nums[n-1]
    // 针对n的奇偶情况分别讨论，得到索引转换关系
    // 将方法二的2.3.步骤合并
    class ssolution_324_3 {
        Random random = new Random();

        public void wiggleSort(int[] nums) {
            // 1. 利用快排找到nums数组的中间值，此时数组整体不一定有序
            int n = nums.length;
            int x = (n + 1) / 2;
            int mid = x - 1;
            int target = findKthLargest(nums, n - mid);

            // 2. 处理较大子数组最小值或较小子数组最大值，等于中间值的情况（三指针）
            // 3. 原地修改得到最终答案
            for (int k = 0, i = 0, j = n - 1; k <= j; k++) {
                if (nums[transAddress(k, n)] > target) {
                    while (j > k && nums[transAddress(j, n)] > target)
                        j--;
                    swap(nums, transAddress(k, n), transAddress(j--, n));
                }
                if (nums[transAddress(k, n)] < target)
                    swap(nums, transAddress(k, n), transAddress(i++, n));
            }
        }

        // 按奇偶情况分类讨论后，偶数mod(n+1)，奇数mod(n)
        public int transAddress(int i, int n) {
            return (2 * n - 2 * i - 1) % (n | 1);
        }

        public int findKthLargest(int[] nums, int k) {
            return quickSelect(nums, 0, nums.length - 1, nums.length - k);
        }

        public int quickSelect(int[] a, int l, int r, int index) {
            int q = randomPartition(a, l, r);
            if (q == index) {
                return a[q];
            } else {
                return q < index ? quickSelect(a, q + 1, r, index) : quickSelect(a, l, q - 1, index);
            }
        }

        public int randomPartition(int[] a, int l, int r) {
            int i = random.nextInt(r - l + 1) + l;
            swap(a, i, r);
            return partition(a, l, r);
        }

        public int partition(int[] a, int l, int r) {
            int x = a[r], i = l - 1;
            for (int j = l; j < r; ++j) {
                if (a[j] <= x) {
                    swap(a, ++i, j);
                }
            }
            swap(a, i + 1, r);
            return i + 1;
        }

        public void swap(int[] a, int i, int j) {
            int temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
    }

    // 方法四：三向切分 + 索引转换 + 递归优化-时间复杂度：O(n)，空间复杂度：O(1)
    // 在方法三的基础上对查找数组中排序为第 k 大元素的函数进行优化，用非递归实现查找第 k 大的元素，进一步优化空间复杂度。
    // （查找第k个的快排可以从递归改写为二分）
    class ssolution_324_4 {
        Random random = new Random();

        public void wiggleSort(int[] nums) {
            int n = nums.length;
            int x = (n + 1) / 2;
            int mid = x - 1;
            int target = findKthLargest(nums, n - mid);
            for (int k = 0, i = 0, j = n - 1; k <= j; k++) {
                if (nums[transAddress(k, n)] > target) {
                    while (j > k && nums[transAddress(j, n)] > target)
                        j--;
                    swap(nums, transAddress(k, n), transAddress(j--, n));
                }
                if (nums[transAddress(k, n)] < target)
                    swap(nums, transAddress(k, n), transAddress(i++, n));
            }
        }

        public int transAddress(int i, int n) {
            return (2 * n - 2 * i - 1) % (n | 1);
        }

        public int findKthLargest(int[] nums, int k) {
            int left = 0, right = nums.length - 1;

            while (left <= right) {
                int pivot = random.nextInt(right - left + 1) + left;
                int newPivot = partitionAroundPivot(left, right, pivot, nums);
                if (newPivot == k - 1) {
                    return nums[newPivot];
                } else if (newPivot > k - 1) {
                    right = newPivot - 1;
                } else {
                    left = newPivot + 1;
                }
            }
            return nums[k - 1];
        }

        public int partitionAroundPivot(int left, int right, int pivot, int[] nums) {
            int pivotValue = nums[pivot];
            int newPivot = left;
            swap(nums, pivot, right);
            for (int i = left; i < right; ++i) {
                if (nums[i] > pivotValue) {
                    swap(nums, i, newPivot++);
                }
            }
            swap(nums, right, newPivot);
            return newPivot;
        }

        public void swap(int[] a, int i, int j) {
            int temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
    }

    // 326.3的幂
    // 给定一个整数，写一个函数来判断它是否是 3 的幂次方。如果是，返回 true ；否则，返回 false 。
    // 整数 n 是 3 的幂次方需满足：存在整数 x 使得 n == 3^x
    // 提示：
    // -231 <= n <= 231 - 1
    // 进阶：你能不使用循环或者递归来完成本题吗？

    // 方法一：试除法-时间复杂度：O(logn)，空间复杂度：O(1)
    public boolean isPowerOfThree(int n) {
        while (n != 0 && n % 3 == 0)
            n /= 3;

        return n == 1;
    }

    // 方法二：判断是否为最大 3 的幂的约数（32位整型下）-时间复杂度：O(1)，空间复杂度：O(1)
    public boolean isPowerOfThree2(int n) {
        return n > 0 && 1162261467 % n == 0;
    }

    // 328.奇偶链表
    // 给定单链表的头节点 head ，将所有索引为奇数的节点和索引为偶数的节点分别组合在一起，然后返回重新排序的列表。
    // 第一个节点的索引被认为是 奇数 ， 第二个节点的索引为 偶数 ，以此类推。
    // 请注意，偶数组和奇数组内部的相对顺序应该与输入时保持一致。
    // 你必须在 O(1) 的额外空间复杂度和 O(n) 的时间复杂度下解决这个问题。
    // 提示:
    // n == 链表中的节点数
    // 0 <= n <= 104
    // -106 <= Node.val <= 106

    // 方法一：分离节点后合并（迭代）时间复杂度：O(n)，空间复杂度：O(1)
    public ListNode oddEvenList(ListNode head) {
        if (head == null)
            return head;

        ListNode evenHead = head.next;
        ListNode odd = head, even = evenHead;
        while (even != null && even.next != null) {
            odd.next = even.next;
            odd = odd.next;
            even.next = odd.next;
            even = even.next;
        }
        odd.next = evenHead;
        return head;
    }

    // 329.矩阵中的最长递增路径
    // 给定一个 m x n 整数矩阵 matrix ，找出其中 最长递增路径 的长度。
    // 对于每个单元格，你可以往上，下，左，右四个方向移动。 你 不能 在 对角线 方向上移动或移动到 边界外（即不允许环绕）。
    // 提示：
    // m == matrix.length
    // n == matrix[i].length
    // 1 <= m, n <= 200
    // 0 <= matrix[i][j] <= 231 - 1

    // 方法一：记忆化（避免重复运算）+ 深度优先搜索-时间复杂度：O(mn)，空间复杂度：O(mn)
    // 由题意特殊性，不需要visited数组避环
    class ssoltion_329_1 {
        public int[][] dirs = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };
        public int rows, columns;

        public int longestIncreasingPath(int[][] matrix) {
            if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
                return 0;

            rows = matrix.length;
            columns = matrix[0].length;
            int[][] memo = new int[rows][columns];
            int ans = 0;
            for (int i = 0; i < rows; ++i)
                for (int j = 0; j < columns; ++j)
                    ans = Math.max(ans, dfs(matrix, i, j, memo));

            return ans;
        }

        public int dfs(int[][] matrix, int row, int column, int[][] memo) {
            if (memo[row][column] != 0)
                return memo[row][column];

            ++memo[row][column];
            for (int[] dir : dirs) {
                int newRow = row + dir[0], newColumn = column + dir[1];
                if (newRow >= 0 && newRow < rows && newColumn >= 0 && newColumn < columns
                        && matrix[newRow][newColumn] > matrix[row][column])
                    memo[row][column] = Math.max(memo[row][column], dfs(matrix, newRow, newColumn, memo) + 1);
            }
            return memo[row][column];
        }
    }

    // 方法二：拓扑排序-时间复杂度：O(mn)，空间复杂度：O(mn)
    // 1. 计算每个单元格对应的出度
    // 2. 拓扑排序，寻找最长递增路径
    class ssoltion_329_2 {
        public int[][] dirs = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };
        public int rows, columns;

        public int longestIncreasingPath(int[][] matrix) {
            if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
                return 0;

            rows = matrix.length;
            columns = matrix[0].length;

            // 1. 计算每个单元格对应的出度
            int[][] outdegrees = new int[rows][columns];
            for (int i = 0; i < rows; ++i)
                for (int j = 0; j < columns; ++j)
                    for (int[] dir : dirs) {
                        int newRow = i + dir[0], newColumn = j + dir[1];
                        if (newRow >= 0 && newRow < rows && newColumn >= 0 && newColumn < columns
                                && matrix[newRow][newColumn] > matrix[i][j])
                            ++outdegrees[i][j];
                    }

            // 出度为0的最大值入队
            Queue<int[]> queue = new LinkedList<int[]>();
            for (int i = 0; i < rows; ++i)
                for (int j = 0; j < columns; ++j)
                    if (outdegrees[i][j] == 0)
                        queue.offer(new int[] { i, j });

            // 2. 拓扑排序，寻找最长递增路径
            int ans = 0;// 拓扑排序层数
            while (!queue.isEmpty()) {
                ++ans;
                int size = queue.size();
                for (int i = 0; i < size; ++i) {
                    int[] cell = queue.poll();
                    int row = cell[0], column = cell[1];
                    for (int[] dir : dirs) {
                        int newRow = row + dir[0], newColumn = column + dir[1];
                        if (newRow >= 0 && newRow < rows && newColumn >= 0 && newColumn < columns
                                && matrix[newRow][newColumn] < matrix[row][column]) {
                            --outdegrees[newRow][newColumn];
                            if (outdegrees[newRow][newColumn] == 0)
                                queue.offer(new int[] { newRow, newColumn });
                        }
                    }
                }
            }
            return ans;
        }
    }

    // 334.递增的三元子序列
    // 给你一个整数数组 nums ，判断这个数组中是否存在长度为 3 的递增子序列。
    // 如果存在这样的三元组下标 (i, j, k) 且满足 i < j < k ，
    // 使得 nums[i] < nums[j] < nums[k] ，返回 true ；否则，返回 false 。
    // 提示：
    // 1 <= nums.length <= 5 * 105
    // -231 <= nums[i] <= 231 - 1
    // 进阶：你能实现时间复杂度为 O(n) ，空间复杂度为 O(1) 的解决方案吗？

    // 方法一：三次遍历-时间复杂度：O(n)，空间复杂度：O(n)
    // 分别一次遍历计算leftMin，rightMax，再遍历判断是否满足leftMin[i - 1] < nums[i] < rightMax[i + 1]
    public boolean increasingTriplet(int[] nums) {
        int n = nums.length;
        if (n < 3)
            return false;

        int[] leftMin = new int[n];
        leftMin[0] = nums[0];
        for (int i = 1; i < n; i++)
            leftMin[i] = Math.min(leftMin[i - 1], nums[i]);

        int[] rightMax = new int[n];
        rightMax[n - 1] = nums[n - 1];
        for (int i = n - 2; i >= 0; i--)
            rightMax[i] = Math.max(rightMax[i + 1], nums[i]);

        for (int i = 1; i < n - 1; i++)
            if (nums[i] > leftMin[i - 1] && nums[i] < rightMax[i + 1])
                return true;

        return false;
    }

    // 方法二：贪心-时间复杂度：O(n)，空间复杂度：O(1)
    // 1. 如果 num>second，则找到了一个递增的三元子序列，返回 true；
    // 2. 否则，如果 num>first，则将 second 的值更新为 num；
    // 3. 否则，将 first 的值更新为 num。
    public boolean increasingTriplet2(int[] nums) {
        int n = nums.length;
        if (n < 3)
            return false;

        int first = nums[0], second = Integer.MAX_VALUE;
        for (int i = 1; i < n; i++) {
            int num = nums[i];
            if (num > second)
                return true;
            else if (num > first)
                second = num;
            else
                first = num;
        }
        return false;
    }

    // 340.至多包含K个不同字符的最长子串
    // 给定一个字符串 s ，找出 至多 包含 k 个不同字符的最长子串 t 。

    // 方法一：滑动窗口-时间复杂度：O(n)，空间复杂度：O(n)
    public int lengthOfLongestSubstringKDistinct(String s, int k) {
        int maxLen = 0; // 记录当前最长子串，初始为0
        int l = 0; // 记录当前左窗口位置
        HashMap<Character, Integer> chars = new HashMap<>();

        for (int r = 0; r < s.length(); r++) {
            // 滑动右指针，直到集合的大小大于k
            if (chars.size() <= k)
                chars.put(s.charAt(r), chars.getOrDefault(s.charAt(r), 0) + 1);

            // 滑动左指针，直到集合大小等于k
            while (chars.size() > k) {
                chars.put(s.charAt(l), chars.get(s.charAt(l)) - 1);
                if (chars.get(s.charAt(l)) <= 0)
                    chars.remove(s.charAt(l));
                l++;
            }
            maxLen = Math.max(maxLen, r - l + 1);
        }
        return maxLen;
    }

    // 341.扁平化嵌套列表迭代器
    // 给你一个嵌套的整数列表 nestedList 。每个元素要么是一个整数，要么是一个列表；该列表的元素也可能是整数或者是其他列表。
    // 请你实现一个迭代器将其扁平化，使之能够遍历这个列表中的所有整数。
    // 实现扁平迭代器类 NestedIterator ：
    // NestedIterator(List<NestedInteger> nestedList) 用嵌套列表 nestedList 初始化迭代器。
    // int next() 返回嵌套列表的下一个整数。
    // boolean hasNext() 如果仍然存在待迭代的整数，返回 true ；否则，返回 false 。
    // 提示：
    // 1 <= nestedList.length <= 500
    // 嵌套列表中的整数值在范围 [-106, 106] 内

    // 方法一：dfs-时间复杂度：初始化为 O(n)，next 和 hasNext 为 O(1)，空间复杂度：O(n)
    class NestedIterator implements Iterator<Integer> {
        private List<Integer> vals;
        private Iterator<Integer> cur;

        public NestedIterator(List<NestedInteger> nestedList) {
            vals = new ArrayList<Integer>();
            dfs(nestedList);
            cur = vals.iterator();
        }

        @Override
        public Integer next() {
            return cur.next();
        }

        @Override
        public boolean hasNext() {
            return cur.hasNext();
        }

        private void dfs(List<NestedInteger> nestedList) {
            for (NestedInteger nest : nestedList) {
                if (nest.isInteger())
                    vals.add(nest.getInteger());
                else
                    dfs(nest.getList());
            }
        }
    }

    // 方法二：栈（类比树的遍历）-时间复杂度：初始化为 O(1)，next 和 hasNext 为 O(1)，空间复杂度：O(n)
    class NestedIterator2 implements Iterator<Integer> {
        // 存储列表的当前遍历位置，实际存储的是当前列表的迭代器，既可以记录遍历位置又可以记录遍历值
        // 即：栈既保存遍历位置，又返回当前遍历值
        private Deque<Iterator<NestedInteger>> stack;

        public NestedIterator2(List<NestedInteger> nestedList) {
            stack = new LinkedList<Iterator<NestedInteger>>();
            stack.push(nestedList.iterator());
        }

        @Override
        public Integer next() {
            // 关键：由于保证调用 next 之前会调用 hasNext，直接返回栈顶列表的当前元素
            return stack.peek().next().getInteger();
        }

        @Override
        public boolean hasNext() {
            while (!stack.isEmpty()) {
                Iterator<NestedInteger> it = stack.peek();
                // 1. 遍历到当前列表末尾，出栈
                if (!it.hasNext()) {
                    stack.pop();
                    continue;
                }
                // 2. 未遍历到当前列表末尾
                NestedInteger nest = it.next();

                // 2.1. 若取出的元素是迭代器，则放入stack，直到找到其中的整数
                if (!nest.isInteger()) {
                    stack.push(nest.getList().iterator());
                } else {
                    // 2.2. 若取出的元素是整数，则通过创建一个额外的列表将其重新放入栈中
                    // 即使是整数也构造成迭代器放入stack，方便next调用
                    List<NestedInteger> list = new ArrayList<NestedInteger>();
                    list.add(nest);
                    stack.push(list.iterator());
                    return true;
                }
            }
            return false;
        }
    }

    // 344.反转字符串
    // 编写一个函数，其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。
    // 不要给另外的数组分配额外的空间，你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。
    // 提示：
    // 1 <= s.length <= 105
    // s[i] 都是 ASCII 码表中的可打印字符

    // 方法一：双指针-时间复杂度：O(n)，空间复杂度：O(1)
    public void reverseString(char[] s) {
        int n = s.length;
        for (int left = 0, right = n - 1; left < right; ++left, --right) {
            char tmp = s[left];
            s[left] = s[right];
            s[right] = tmp;
        }
    }

    // 方法一：双指针（其实就是交换位置）-时间复杂度：O(n)，空间复杂度：O(1)
    public void reverseString11(char[] s) {
        int n = s.length;
        for (int i = 0; i < n >> 1; i++) {
            char temp = s[i];
            s[i] = s[n - 1 - i];
            s[n - 1 - i] = temp;
        }
    }

    // 347.前K个高频元素（哈希表计数后，问题转化为「215. 数组中的第K个最大元素」）
    // 给你一个整数数组 nums 和一个整数 k ，请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
    // 提示：
    // 1 <= nums.length <= 105
    // k 的取值范围是 [1, 数组中不相同的元素的个数]
    // 题目数据保证答案唯一，换句话说，数组中前 k 个高频元素的集合是唯一的
    // 进阶：你所设计算法的时间复杂度 必须 优于 O(n log n) ，其中 n 是数组大小。

    // 方法一：哈希表+堆-时间复杂度：O(Nlogk)，空间复杂度：O(N)
    // 首先遍历整个数组，并使用哈希表记录每个数字出现的次数，并形成一个「出现次数数组」。
    // 找出原数组的前 k 个高频元素，就相当于找出「出现次数数组」的前 k 大的值。
    // 最简单的做法是给「出现次数数组」排序。但由于可能有 O(N) 个不同的出现次数（其中 N 为原数组长度），
    // 故总的算法复杂度会达到 O(NlogN)，不满足题目的要求。

    // 选取k个最大值的另一种思路：（不是数组映射到根数，而是直接使用优先队列）
    // 建立一个小顶堆，然后遍历「出现次数数组」：
    // 如果堆的元素个数小于 k，就可以直接插入堆中。
    // 如果堆的元素个数等于 k，则检查堆顶与当前出现次数的大小。
    // 如果堆顶更大，说明至少有 k 个数字的出现次数比当前值大，故舍弃当前值；否则，就弹出堆顶，并将当前值插入堆中。
    // 遍历完成后，堆中的元素就代表了「出现次数数组」中前 k 大的值。
    public int[] topKFrequent(int[] nums, int k) {
        // 哈希表记录每个数字出现的次数，并形成一个「出现次数数组」。
        Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
        for (int num : nums)
            occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);

        // int[] 的第一个元素代表数组的值，第二个元素代表了该值出现的次数
        PriorityQueue<int[]> queue = new PriorityQueue<int[]>((m, n) -> m[1] - n[1]);

        for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
            int num = entry.getKey(), count = entry.getValue();
            if (queue.size() == k) {
                if (queue.peek()[1] < count) {// 弃掉堆顶和当前值中更小的
                    queue.poll();
                    queue.offer(new int[] { num, count });
                }

            } else// 优先队列中不足k个则直接入队
                queue.offer(new int[] { num, count });

        }

        int[] ret = new int[k];
        for (int i = 0; i < k; ++i)
            ret[i] = queue.poll()[0];

        return ret;

    }

    // 方法二：哈希表+基于快速排序-时间复杂度：O(N^2)（最坏），空间复杂度：O(N)
    // 使用基于快速排序的方法，求出「出现次数数组」的前 k 大的值。
    public int[] topKFrequent2(int[] nums, int k) {
        Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
        for (int num : nums)
            occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);

        // entrySet转List
        List<int[]> values = new ArrayList<int[]>();
        for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
            int num = entry.getKey(), count = entry.getValue();
            values.add(new int[] { num, count });
        }

        int[] ret = new int[k];
        qsort(values, 0, values.size() - 1, ret, 0, k);
        return ret;
    }

    // 选边快排（降序，递归形式）
    public void qsort(List<int[]> values, int start, int end, int[] ret, int retIndex, int k) {
        // 还是随机选择pivot
        int picked = (int) (Math.random() * (end - start + 1)) + start;
        int pivot = values.get(picked)[1];
        Collections.swap(values, picked, start);

        // 快排，确定pivot的最终位置，即index
        int index = start;
        for (int i = start + 1; i <= end; i++)
            if (values.get(i)[1] >= pivot) {
                Collections.swap(values, index + 1, i);
                index++;
            }

        Collections.swap(values, start, index);

        if (k <= index - start) {// pivot最终位置不在前k以内，则不是要找的答案，继续对左半边进行递归
            qsort(values, start, index - 1, ret, retIndex, k);
        } else {// pivot最终位置在前k以内
            for (int i = start; i <= index; i++) // index之前的放入答案ret
                ret[retIndex++] = values.get(i)[0];

            if (k > index - start + 1) // k = index - start时，全部答案已找到
                // 问题转化为查找右半边的前 k - (index - start + 1) 个最大值
                qsort(values, index + 1, end, ret, retIndex, k - (index - start + 1));

        }
    }

    // 348.设计井字棋
    // 请在 n × n 的棋盘上，实现一个判定井字棋（Tic-Tac-Toe）胜负的神器，判断每一次玩家落子后，是否有胜出的玩家。
    // 在这个井字棋游戏中，会有 2 名玩家，他们将轮流在棋盘上放置自己的棋子。
    // 在实现这个判定器的过程中，你可以假设以下这些规则一定成立：
    // 1. 每一步棋都是在棋盘内的，并且只能被放置在一个空的格子里；
    // 2. 一旦游戏中有一名玩家胜出的话，游戏将不能再继续；
    // 3. 一个玩家如果在同一行、同一列或者同一斜对角线上都放置了自己的棋子，那么他便获得胜利。

    // 方法一：分别统计每行每列对角线的棋子-时间复杂度：move函数，O(1)，空间复杂度：O(n)
    class TicTacToe {
        private int[] rows;
        private int[] cols;
        private int[] dig;

        private int n;// 棋盘的大小为 n × n

        /**
         * Initialize your data structure here.
         */
        public TicTacToe(int n) {
            rows = new int[n];
            cols = new int[n];
            dig = new int[2];
            this.n = n;
        }

        /**
         * Player {player} makes a move at ({row}, {col}).
         *
         * @param row    The row of the board.
         * @param col    The column of the board.
         * @param player The player, can be either 1 or 2.
         * @return The current winning condition, can be either:
         *         0: No one wins.
         *         1: Player 1 wins.
         *         2: Player 2 wins.
         */
        public int move(int row, int col, int player) {
            boolean gameOver = false;
            if (player == 1) { // 1 号玩家
                rows[row]++; // 1 号玩家在这一行新加了一个棋子，用 +1 表示
                cols[col]++;
                if (row == col)
                    dig[0]++;
                if (row + col == n - 1)
                    dig[1]++;
                gameOver = rows[row] == n || // 若第 row 行全是玩家 1 的棋子，那么 rows[row] 的值就是 n
                        cols[col] == n || // 若第 col 行全是玩家 1 的棋子，那么 cols[col] 的值就是 n
                        (row == col && dig[0] == n) || // 若左上到右下全是玩家 1 的棋子，那么 dig[0] 的值就是 n
                        (row + col == n - 1 && dig[1] == n); // 若右上到左下全是玩家 1 的棋子，那么 dig[1] 的值就是 n
                return gameOver ? player : 0;
            } else { // 2 号玩家
                rows[row]--; // 2 号玩家在这一行新加了一个棋子，用 -1 表示
                cols[col]--;
                if (row == col)
                    dig[0]--;
                if (row + col == n - 1)
                    dig[1]--;
                gameOver = rows[row] == -n ||
                        cols[col] == -n ||
                        (row == col && dig[0] == -n) ||
                        (row + col == n - 1 && dig[1] == -n);
                return gameOver ? player : 0;
            }
        }
    }

    // 350.两个数组的交集 II
    // 给你两个整数数组 nums1 和 nums2 ，请你以数组形式返回两数组的交集。
    // 返回结果中每个元素出现的次数，应与元素在两个数组中都出现的次数一致（如果出现次数不一致，则考虑取较小值）。
    // 可以不考虑输出结果的顺序。
    // 提示：
    // 1 <= nums1.length, nums2.length <= 1000
    // 0 <= nums1[i], nums2[i] <= 1000
    // 进阶：
    // 如果给定的数组已经排好序呢？你将如何优化你的算法？
    // 如果 nums1 的大小比 nums2 小，哪种方法更优？
    // 如果 nums2 的元素存储在磁盘上，内存是有限的，并且你不能一次加载所有的元素到内存中，你该怎么办？

    // 方法一：哈希表-时间复杂度：O(m+n)，空间复杂度：O(min(m,n))
    public int[] intersect(int[] nums1, int[] nums2) {
        if (nums1.length > nums2.length)
            return intersect(nums2, nums1);

        Map<Integer, Integer> map = new HashMap<Integer, Integer>();
        for (int num : nums1) {
            int count = map.getOrDefault(num, 0) + 1;
            map.put(num, count);
        }

        int[] intersection = new int[nums1.length];
        int index = 0;
        for (int num : nums2) {
            int count = map.getOrDefault(num, 0);
            if (count > 0) {
                intersection[index++] = num;
                count--;
                if (count > 0)
                    map.put(num, count);
                else
                    map.remove(num);

            }
        }
        return Arrays.copyOfRange(intersection, 0, index);
    }

    // 如果给定的数组已经排好序呢？你将如何优化你的算法？
    // 方法二：排序 + 双指针-时间复杂度：O(mlogm+nlogn)，空间复杂度：O(min(m,n))
    public int[] intersect2(int[] nums1, int[] nums2) {
        Arrays.sort(nums1);
        Arrays.sort(nums2);
        int length1 = nums1.length, length2 = nums2.length;
        int[] intersection = new int[Math.min(length1, length2)];
        int index1 = 0, index2 = 0, index = 0;
        while (index1 < length1 && index2 < length2) {
            if (nums1[index1] < nums2[index2])
                index1++;
            else if (nums1[index1] > nums2[index2])
                index2++;
            else {
                intersection[index] = nums1[index1];
                index1++;
                index2++;
                index++;
            }
        }
        return Arrays.copyOfRange(intersection, 0, index);
    }

    // 如果 nums2 的元素存储在磁盘上，磁盘内存是有限的，并且你不能一次加载所有的元素到内存中。
    // 那么就无法高效地对 nums2 进行排序，因此推荐使用方法一而不是方法二。
    // 在方法一中，nums2 只关系到查询操作，因此每次读取 nums2 中的一部分数据，并进行处理即可。

    // 371.两整数之和
    // 给你两个整数 a 和 b ，不使用 运算符 + 和 - ​​​​​​​，计算并返回两整数之和。
    // 提示：
    // -1000 <= a, b <= 1000

    // 方法一：位运算-时间复杂度：O(1)，空间复杂度：O(1)
    public int getSum(int a, int b) {
        while (b != 0) {
            int carry = (a & b) << 1;// 进位
            a = a ^ b;// 未进位的结果
            b = carry;// 进入下一层循环，加上进位
        }
        return a;
    }

    // 378.有序矩阵中第K小的元素
    // 给你一个 n x n 矩阵 matrix ，其中每行和每列元素均按升序排序，找到矩阵中第 k 小的元素。
    // 请注意，它是 排序后 的第 k 小元素，而不是第 k 个 不同 的元素。
    // 你必须找到一个内存复杂度优于 O(n2) 的解决方案。
    // 提示：
    // n == matrix.length
    // n == matrix[i].length
    // 1 <= n <= 300
    // -109 <= matrix[i][j] <= 109
    // 题目数据 保证 matrix 中的所有行和列都按 非递减顺序 排列
    // 1 <= k <= n2
    // 进阶：
    // 你能否用一个恒定的内存(即 O(1) 内存复杂度)来解决这个问题?
    // 你能在 O(n) 的时间复杂度下解决这个问题吗?这个方法对于面试来说可能太超前了，但是你会发现阅读这篇文章（ this paper ）很有趣。

    // 方法一：展开矩阵排序，返回第 k 个数（完全没有利用有序矩阵的特性）-时间复杂度：O(n2logn)，空间复杂度：O(1)

    // 方法二：归并排序（利用了有序矩阵的部分特性，即每行有序）-时间复杂度：O(nlogn)，空间复杂度：O(n)

    // 方法二：优化归并排序，使用优先队列（23. 合并K个排序链表）--时间复杂度：O(klogn)，空间复杂度：O(n)
    public int kthSmallest2(int[][] matrix, int k) {
        // 优先队列内的数组元素依次为：[值，行数，列数]
        PriorityQueue<int[]> pq = new PriorityQueue<int[]>((a, b) -> (a[0] - b[0]));
        int n = matrix.length;
        for (int i = 0; i < n; i++)
            pq.offer(new int[] { matrix[i][0], i, 0 });

        for (int i = 0; i < k - 1; i++) {
            int[] now = pq.poll();
            if (now[2] != n - 1)
                pq.offer(new int[] { matrix[now[1]][now[2] + 1], now[1], now[2] + 1 });
        }
        return pq.poll()[0];// 出队 k-1 个数后，堆顶即是第 k 个数
    }

    // 方法三：二分查找-时间复杂度：O(nlog(max-min))，二分查找进行次数为O(log(max−min))，每次操作时间复杂度为O(n)，空间复杂度：O(1)
    public int kthSmallest3(int[][] matrix, int k) {
        int n = matrix.length;
        // 二分范围为矩阵中的最小值到最大值
        int left = matrix[0][0], right = matrix[n - 1][n - 1];

        while (left <= right) {
            int mid = (left + right) / 2;

            // 计算小于等于mid的数有多少个，跟k比较
            int count = 0;
            int i = 0, j = n - 1;
            while (i < n && 0 <= j) {
                if (matrix[i][j] > mid) {
                    j--;
                } else {
                    count += j + 1;
                    i++;
                }
            }

            if (count < k)
                left = mid + 1;
            else// 当count==k时，此时矩阵中可能没有恰好等于mid的值，不能直接返回mid，需要继续二分
                right = mid - 1;
        }
        return left;
    }

    // 380.0(1)时间插入、删除和获取
    // 实现RandomizedSet 类：
    // RandomizedSet() 初始化 RandomizedSet 对象
    // bool insert(int val) 当元素 val 不存在时，向集合中插入该项，并返回 true ；否则，返回 false 。
    // bool remove(int val) 当元素 val 存在时，从集合中移除该项，并返回 true ；否则，返回 false 。
    // int getRandom() 随机返回现有集合中的一项（测试用例保证调用此方法时集合中至少存在一个元素）。每个元素应该有 相同的概率 被返回。
    // 你必须实现类的所有函数，并满足每个函数的 平均 时间复杂度为 O(1) 。 
    // 提示：
    // -231 <= val <= 231 - 1
    // 最多调用 insert、remove 和 getRandom 函数 2 * 105 次
    // 在调用 getRandom 方法时，数据结构中 至少存在一个 元素。

    // 方法一：变长数组 + 哈希表-时间复杂度：O(1)，空间复杂度：O(n)
    // 删除操作的重点在于将变长数组的最后一个元素移动到待删除元素的下标处，然后删除变长数组的最后一个元素。
    class RandomizedSet {
        List<Integer> nums;// <index, value>
        Map<Integer, Integer> indices;// <value, index>
        Random random;

        public RandomizedSet() {
            nums = new ArrayList<Integer>();
            indices = new HashMap<Integer, Integer>();
            random = new Random();
        }

        public boolean insert(int val) {
            // 1. 如果已经存在则返回 false
            if (indices.containsKey(val))
                return false;

            // 2. 如果不存在则插入 val
            int index = nums.size();
            nums.add(val);// 在变长数组的末尾添加 val
            indices.put(val, index);// 在添加 val 之前的变长数组长度为所在下标 index，将 val 和下标 index 存入哈希表
            return true;
        }

        // 删除操作的重点在于将变长数组的最后一个元素移动到待删除元素的下标处，然后删除变长数组的最后一个元素。
        public boolean remove(int val) {
            // 1. 如果不存在则返回 false
            if (!indices.containsKey(val))
                return false;

            // 2. 如果存在则删除 val
            int index = indices.get(val);// 从哈希表中获得 val 的下标 index

            // 将变长数组的最后一个元素 last 移动到下标 index 处，在哈希表中将 last 的下标更新为 index
            int last = nums.get(nums.size() - 1);
            nums.set(index, last);
            indices.put(last, index);

            // 在变长数组中删除最后一个元素，在哈希表中删除 val
            nums.remove(nums.size() - 1);
            indices.remove(val);
            return true;
        }

        public int getRandom() {
            int randomIndex = random.nextInt(nums.size());
            return nums.get(randomIndex);
        }
    }

    // 384.打乱数组
    // 给你一个整数数组 nums ，设计算法来打乱一个没有重复元素的数组。打乱后，数组的所有排列应该是 等可能 的。
    // 实现 Solution class:
    // Solution(int[] nums) 使用整数数组 nums 初始化对象
    // int[] reset() 重设数组到它的初始状态并返回
    // int[] shuffle() 返回数组随机打乱后的结果
    // 提示：
    // 1 <= nums.length <= 50
    // -106 <= nums[i] <= 106
    // nums 中的所有元素都是 唯一的
    // 最多可以调用 104 次 reset 和 shuffle

    // 方法一：Fisher-Yates 洗牌算法（与380.删除元素类似，即交换末尾元素）-时间复杂度：O(n)，空间复杂度：O(n)
    class Solution {
        int[] nums;
        int[] original;

        public Solution(int[] nums) {
            this.nums = nums;
            this.original = new int[nums.length];
            System.arraycopy(nums, 0, original, 0, nums.length);
        }

        public int[] reset() {
            System.arraycopy(original, 0, nums, 0, nums.length);
            return nums;
        }

        public int[] shuffle() {
            Random random = new Random();
            for (int i = 0; i < nums.length; ++i) {
                // 在[0, nums.length - i]范围内选择随机数
                int j = i + random.nextInt(nums.length - i);

                // 选到的数移动到数组末尾
                int temp = nums[i];
                nums[i] = nums[j];
                nums[j] = temp;
            }
            return nums;
        }
    }

    // 387.字符串中的第一个唯一字符
    // 给定一个字符串 s ，找到 它的第一个不重复的字符，并返回它的索引 。如果不存在，则返回 -1 。
    // 提示:
    // 1 <= s.length <= 105
    // s 只包含小写字母

    // 方法一：使用哈希表存储频数（两次遍历）-时间复杂度：O(n)，空间复杂度：O(1)
    public int firstUniqChar(String s) {
        Map<Character, Integer> frequency = new HashMap<Character, Integer>();
        for (int i = 0; i < s.length(); ++i) {
            char ch = s.charAt(i);
            frequency.put(ch, frequency.getOrDefault(ch, 0) + 1);
        }

        for (int i = 0; i < s.length(); ++i)
            if (frequency.get(s.charAt(i)) == 1)
                return i;

        return -1;
    }

    // 方法二：使用哈希表存储索引（两次遍历）-时间复杂度：O(n)，空间复杂度：O(1)
    public int firstUniqChar2(String s) {
        int n = s.length();
        Map<Character, Integer> position = new HashMap<Character, Integer>();

        for (int i = 0; i < n; ++i) {
            char ch = s.charAt(i);
            if (position.containsKey(ch))
                position.put(ch, Integer.MAX_VALUE);
            else
                position.put(ch, i);
        }

        int res = n;
        for (Map.Entry<Character, Integer> entry : position.entrySet()) {
            int pos = entry.getValue();
            res = Math.min(res, pos);
        }

        return res == n ? -1 : res;
    }

    // 方法三：使用哈希表存储索引 + 队列（两次遍历）-时间复杂度：O(n)，空间复杂度：O(1)
    public int firstUniqChar3(String s) {
        int n = s.length();
        Map<Character, Integer> position = new HashMap<Character, Integer>();
        Queue<Character> queue = new ArrayDeque<>();

        for (int i = 0; i < n; ++i) {
            char ch = s.charAt(i);
            if (position.containsKey(ch))
                position.put(ch, -1);
            else {
                position.put(ch, i);
                queue.offer(ch);
            }
        }

        while (!queue.isEmpty()) {
            char ch = queue.poll();
            int index = position.get(ch);
            if (index != -1)
                return index;
        }
        return -1;
    }

    // 395.至少有K个重复字符的最长子串
    // 给你一个字符串 s 和一个整数 k ，请你找出 s 中的最长子串， 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。
    // 提示：
    // 1 <= s.length <= 104
    // s 仅由小写英文字母组成
    // 1 <= k <= 105

    // 方法一：分治
    // 时间复杂度：O(N⋅|Σ|)，其中 N 为字符串的长度，Σ 为字符集
    // 空间复杂度：O(|Σ|^2)，递归的深度为 O(|Σ|)，每层递归需要开辟 O(|Σ|) 的额外空间。

    // 对于字符串 s，如果存在某个字符 ch，它的出现次数大于 0 且小于 k，则任何包含 ch 的子串都不可能满足要求。
    // 也就是说，我们将字符串按照 ch 切分成若干段，则满足要求的最长子串一定出现在某个被切分的段内，而不能跨越一个或多个段。
    class ssoltion_395 {
        public int longestSubstring(String s, int k) {
            int n = s.length();
            return dfs(s, 0, n - 1, k);
        }

        public int dfs(String s, int l, int r, int k) {
            // 遍历整个区间[l,r]，记录每个字符的出现频率
            int[] cnt = new int[26];
            for (int i = l; i <= r; i++)
                cnt[s.charAt(i) - 'a']++;

            // 寻找第一个不满足出现频率k的字符split，以此作为分割
            char split = 0;
            for (int i = 0; i < 26; i++) {
                if (cnt[i] > 0 && cnt[i] < k) {
                    split = (char) (i + 'a');
                    break;
                }
            }
            // 未找到不满足出现频率k的字符，即整个区间都是满足要求的，直接返回
            if (split == 0)
                return r - l + 1;

            int ret = 0;
            int newRight = l;
            // 保证进入下一层dfs的区间内没有split字符（以split字符的下标划分字符串进入下一层dfs会栈溢出，即递归层数过多）
            while (newRight <= r) {
                while (newRight <= r && s.charAt(newRight) == split)
                    newRight++;

                if (newRight > r)
                    break;

                int newLeft = newRight;
                while (newRight <= r && s.charAt(newRight) != split)
                    newRight++;

                int length = dfs(s, newLeft, newRight - 1, k);
                ret = Math.max(ret, length);
            }
            return ret;
        }
    }

    // 方法二：滑动窗口（需要遍历|Σ|次字符串，一次遍历是做不到的）
    // 时间复杂度：O(N⋅|Σ|+|Σ|^2)，其中 N 为字符串的长度，Σ 为字符集，本题中字符串仅包含小写字母，因此 |Σ|=26。
    // 我们需要遍历所有可能的 t，共 |Σ| 种可能性；内层循环中滑动窗口的复杂度为 O(N)，且初始时需要 O(|Σ|) 的时间初始化 cnt 数组。
    // 空间复杂度：O(|Σ|)
    public int longestSubstring(String s, int k) {
        int ret = 0;
        int n = s.length();

        // 枚举最长子串中的字符种类数目
        for (int t = 1; t <= 26; t++) {
            int[] cnt = new int[26];// 滑动窗口内部每个字符出现的次数
            int tot = 0;// 滑动窗口内的字符种类数目
            int less = 0;// 计数器，代表当前出现次数小于 k 的字符的数量。（避免每次判断滑动窗口内字符出现次数都大于k而遍历一次cnt数组）
            int l = 0, r = 0;
            while (r < n) {
                cnt[s.charAt(r) - 'a']++;
                if (cnt[s.charAt(r) - 'a'] == 1) {// 某个字符的出现次数从 0 增加到 1
                    tot++;
                    less++;
                }
                if (cnt[s.charAt(r) - 'a'] == k)// 当某个字符的出现次数从 k−1 增加到 k
                    less--;

                while (tot > t) {// tot > t 时，右移左边界 l ，直到 tot == t
                    cnt[s.charAt(l) - 'a']--;
                    if (cnt[s.charAt(l) - 'a'] == k - 1)// 某个字符的出现次数从 k 减少到 k-1
                        less++;

                    if (cnt[s.charAt(l) - 'a'] == 0) {// 某个字符的出现次数从 1 减少到 0
                        tot--;
                        less--;
                    }
                    l++;
                }

                if (less == 0)// 此时 tot == t，且滑动窗口内的字符出现次数都大于k，满足条件
                    ret = Math.max(ret, r - l + 1);

                r++;
            }
        }
        return ret;
    }

    // 412. Fizz Buzz
    // 给你一个整数 n ，找出从 1 到 n 各个整数的 Fizz Buzz 表示，并用字符串数组 answer（下标从 1 开始）返回结果，其中：
    // answer[i] == "FizzBuzz" 如果 i 同时是 3 和 5 的倍数。
    // answer[i] == "Fizz" 如果 i 是 3 的倍数。
    // answer[i] == "Buzz" 如果 i 是 5 的倍数。
    // answer[i] == i （以字符串形式）如果上述条件全不满足。
    // 提示：
    // 1 <= n <= 104

    // 方法一：模拟 + 字符串拼接-时间复杂度：O(n)，空间复杂度：O(1)
    public List<String> fizzBuzz1(int n) {
        List<String> answer = new ArrayList<String>();
        for (int i = 1; i <= n; i++) {
            StringBuffer sb = new StringBuffer();
            if (i % 3 == 0)
                sb.append("Fizz");

            if (i % 5 == 0)
                sb.append("Buzz");

            if (sb.length() == 0)
                sb.append(i);

            answer.add(sb.toString());
        }
        return answer;
    }

    // 方法一：模拟 + 字符串拼接（自己写的）-时间复杂度：O(n)，空间复杂度：O(1)
    public List<String> fizzBuzz2(int n) {
        List<String> res = new ArrayList<>();
        for (int i = 1; i <= n; i++) {
            boolean isMultipleOf3 = i % 3 == 0;
            boolean isMultipleOf5 = i % 5 == 0;

            if (isMultipleOf3 && isMultipleOf5)
                res.add("FizzBuzz");
            else if (isMultipleOf3)
                res.add("Fizz");
            else if (isMultipleOf5)
                res.add("Buzz");
            else
                res.add(String.valueOf(i));
        }
        return res;
    }

    // 454.四数相加 II
    // 给你四个整数数组 nums1、nums2、nums3 和 nums4 ，数组长度都是 n ，请你计算有多少个元组 (i, j, k, l) 能满足：
    // 0 <= i, j, k, l < n
    // nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0
    // 提示：
    // n == nums1.length
    // n == nums2.length
    // n == nums3.length
    // n == nums4.length
    // 1 <= n <= 200
    // -228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228

    // 方法一：分组 + 哈希表-时间复杂度：O(n^2)，空间复杂度：O(n^2)
    public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
        Map<Integer, Integer> countAB = new HashMap<Integer, Integer>();
        for (int u : A)
            for (int v : B)
                countAB.put(u + v, countAB.getOrDefault(u + v, 0) + 1);

        int ans = 0;
        for (int u : C)
            for (int v : D)
                ans += countAB.getOrDefault(-u - v, 0);

        return ans;
    }

    // 方法一：分组 + 哈希表（自己写的）-时间复杂度：O(n^2)，空间复杂度：O(n^2)
    public int fourSumCount11(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        int res = 0;
        Map<Integer, Integer> map1 = count(nums1);
        Map<Integer, Integer> map2 = count(nums2);
        Map<Integer, Integer> map3 = count(nums3);
        Map<Integer, Integer> map4 = count(nums4);
        Map<Integer, Integer> map12 = getSumCount(map1, map2);
        Map<Integer, Integer> map123 = getSumCount(map12, map3);
        for (Map.Entry<Integer, Integer> entry : map4.entrySet()) {
            res += map123.getOrDefault(-entry.getKey(), 0) * entry.getValue();
        }
        return res;
    }

    private Map<Integer, Integer> count(int[] nums) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums)
            map.put(num, map.getOrDefault(num, 0) + 1);
        return map;
    }

    private Map<Integer, Integer> getSumCount(Map<Integer, Integer> map1, Map<Integer, Integer> map2) {
        Map<Integer, Integer> map = new HashMap<>();
        for (Map.Entry<Integer, Integer> entry1 : map1.entrySet()) {
            for (Map.Entry<Integer, Integer> entry2 : map2.entrySet()) {
                int key = entry1.getKey() + entry2.getKey();
                int value = entry1.getValue() * entry2.getValue();
                map.put(key, map.getOrDefault(key, 0) + value);
            }
        }
        return map;
    }

    // 方法二：dfs 递归-时间复杂度：O(n^4)，空间复杂度：O(1)
    // 用哈希表优化后还是超时

}

class ssoltion {
    
}

public class SelectedTopInterview {
    public static void main(String[] args) {

        // int[][] matrix = new int[][] { { 9, 9, 4 }, { 6, 6, 8 }, { 2, 1, 1 } };
        // System.out.println(new ssoltion().longestSubstring("baaabcb", 3));
        // System.out.println(-(long) Integer.MIN_VALUE);
    }

}
